From 9b8b5ec5333b994e7140bac7ec095c6f9bc3319a Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Fri, 14 Nov 2025 21:56:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=81=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B0=D0=BA=D0=BE=D0=BF?= =?UTF-8?q?=D0=B8=D0=B2=D1=88=D0=B8=D0=B5=D1=81=D1=8F=20=D1=81=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BB=D0=B8=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MCU Wrapper.mltbx | Bin 83927 -> 94171 bytes McuLib/install_my_library.m | 14 +- McuLib/lib/McuLib.slx | Bin 22975 -> 24145 bytes McuLib/m/appWrap.m | 129 ++++++++++++----- McuLib/m/asynchManage.m | 67 ++++++--- McuLib/m/compiler.m | 96 ++++++++----- McuLib/m/configJs.m | 82 +++++++---- McuLib/m/customtable.m | 121 +++++++++------- McuLib/m/editCode.m | 113 ++++++++------- McuLib/m/installTemplates.m | 2 +- McuLib/m/mainConfig.m | 101 +++++++------ McuLib/m/mcuMask.m | 155 ++++++++++---------- McuLib/m/mcuPath.m | 123 ++++++++++------ McuLib/m/mcuPorts.m | 143 +++++++++++-------- McuLib/m/mexing.m | 272 +++++++++++++++++++----------------- McuLib/m/periphConfig.m | 271 +++++++++++++++++++---------------- mcuwrapper.prj | 18 +-- 17 files changed, 1008 insertions(+), 699 deletions(-) diff --git a/MCU Wrapper.mltbx b/MCU Wrapper.mltbx index 646b7745e77e327727c8c6618a824c72c9311182..64a60f7cbc1d0d72340ac8628d0dc2684528ae7a 100644 GIT binary patch delta 76049 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 z≷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 delta 65749 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⁡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 diff --git a/McuLib/m/appWrap.m b/McuLib/m/appWrap.m index cfb7a94..0d04173 100644 --- a/McuLib/m/appWrap.m +++ b/McuLib/m/appWrap.m @@ -1,100 +1,144 @@ classdef appWrap + % Класс для редактирования кода обёртки МК прямо внутри MATLAB/Simulink + % Позволяет работать с кодом микроконтроллера без переключения между IDE methods(Static) + function appWrapperFunc() - block = gcb; - % Получаем имя функции и путь к файлам - [filename, section, tool, example]= appWrap.getAppWrapperUserFile(block); + % Загружает код обёртки из файлов в интерфейс MATLAB для редактирования + % Автоматически вызывается при выборе типа кода в маске блока + + block = gcb; % Получаем текущий блок Simulink для редактирования + + % Определяем какой файл и секцию нужно редактировать + [filename, section, tool, example] = appWrap.getAppWrapperUserFile(block); + + % Показываем подсказку по выбранному типу кода прямо в MATLAB mcuMask.tool(tool, example); - % Загружаем содержимое файла + % Очищаем поле редактирования перед загрузкой нового кода set_param(block, 'appWrapperCode', ''); - try - code = fileread(filename); - code = regexprep(code, '\r\n?', '\n'); % нормализуем окончания строк - - includesText = editCode.extractSection(code, section); - set_param(block, 'appWrapperCode', includesText); - catch - end - % % Поиск тела обычной функции - % expr = sprintf('void %s()', sel); - % funcBody = editCode.extractSection(code, expr); - % set_param(block, 'wrapperCode', funcBody); + + % Загружаем код из файла в интерфейс MATLAB + try + % Читаем исходный код из файла + code = fileread(filename); + code = regexprep(code, '\r\n?', '\n'); % унифицируем переводы строк для MATLAB + + % Вырезаем только нужную секцию для редактирования + includesText = editCode.extractSection(code, section); + + % Загружаем код в текстовое поле маски для редактирования + set_param(block, 'appWrapperCode', includesText); + catch + % Если файл не найден или ошибка чтения - оставляем поле пустым + % Пользователь сможет написать код с нуля прямо в MATLAB + end end function saveAppWrapperCode() - block = gcb; + % Сохраняет отредактированный код из MATLAB обратно в файлы обёртки + % Вызывается при нажатии кнопки "Сохранить" в интерфейсе + + block = gcb; % Текущий редактируемый блок - % Получаем имя функции и путь к файлам + % Определяем куда сохранять based на выборе пользователя в MATLAB [filename, section] = appWrap.getAppWrapperUserFile(block); + + % Проверяем что файл существует перед сохранением if ~isfile(filename) - errordlg(['Файл не найден: ', filename]); + errordlg(['Файл не найден: ', filename], 'Ошибка сохранения в MATLAB'); return; end + % Получаем выбранный тип кода и путь к файлам sel = get_param(block, 'appWrapperFunc'); basePath = get_param(block, 'appWrapperPath'); + if isempty(basePath) - errordlg('Не указан путь к файлам обёртки (wrapperPath).'); + errordlg('Не указан путь к файлам обёртки.', 'Ошибка в MATLAB'); return; end + + % Получаем код который пользователь написал/отредактировал в MATLAB newBody = get_param(block, 'appWrapperCode'); + + % Читаем текущее содержимое файла чтобы сохранить структуру code = fileread(filename); code = regexprep(code, '\r\n?', '\n'); + + % Экранируем специальные символы для корректного сохранения из MATLAB newBody = strrep(newBody, '\', '\\'); + + % Вставляем отредактированную в MATLAB секцию обратно в файл 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'); + + % Сохраняем изменения прямо из MATLAB без внешних редакторов + fid = fopen(filename, 'w', 'n'); if fid == -1 - errordlg('Не удалось открыть файл для записи'); + errordlg('Не удалось сохранить файл из MATLAB', 'Ошибка записи'); return; end + + % Записываем обновленный код fwrite(fid, code); fclose(fid); - mcuMask.disp(1, ['Обновлено: ' sel]); + + % Подтверждаем успешное сохранение в интерфейсе MATLAB + mcuMask.disp(1, ['Сохранено в MATLAB: ' sel]); end function openAppWrapperCode() + % Альтернативный способ: открывает файл в системном редакторе + % Для случаев когда нужно работать во внешней IDE + block = gcb; - % Получаем имя функции и путь к файлам + % Получаем абсолютный путь к файлу для открытия filename = mcuPath.getAbsolutePath(appWrap.getAppWrapperUserFile(block)); + if exist(filename, 'file') == 2 - % Формируем команду без кавычек + % Открываем через системный диалог "Открыть с помощью" + % Полезно если нужно использовать привычную IDE вместо редактора MATLAB cmd = sprintf('rundll32.exe shell32.dll,OpenAs_RunDLL %s', filename); status = system(cmd); + if status ~= 0 - errordlg('Не удалось открыть окно выбора приложения.'); + errordlg('Не удалось открыть файл во внешнем редакторе'); end else - errordlg('Файл не найден'); + errordlg('Файл не найден для открытия'); end end -%% SPECIFIC TOOLS + %% SPECIFIC TOOLS function [filename, section, tool, example] = getAppWrapperUserFile(block, sel) + % Маппинг типов кода на файлы и секции для редактирования в MATLAB + % Центральное место настройки - здесь определяется где что редактировать + if (nargin < 2) + % Получаем какой тип кода пользователь выбрал в интерфейсе MATLAB sel = get_param(block, 'appWrapperFunc'); end + % Базовый путь к файлам обёртки (настраивается в маске) basePath = mcuPath.get('appWrapperPath'); if isempty(basePath) - errordlg('Не указан путь к файлам обёртки (wrapperPath).'); + errordlg('Не указан путь к файлам обёртки.'); return; end - % Формируем путь к файлу в зависимости от типа запроса + + % В зависимости от выбора в MATLAB - определяем что редактировать: if strcmp(sel, 'Includes') + % Редактирование подключения заголовочных файлов filename = fullfile(basePath, 'app_includes.h'); section = '// INCLUDES'; tool = 'Инклюды для доступа к коду МК в коде оболочке'; - example = '#include "main.h"'; + example = '#include "main.h"'; % Пример для подсказки в MATLAB + elseif strcmp(sel, 'Dummy') + % Редактирование заглушек для симуляции filename = fullfile(basePath, 'app_wrapper.c'); section = '// DUMMY'; tool = 'Заглушки для различных функций и переменных'; @@ -104,19 +148,25 @@ classdef appWrap ' 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') + % Редактирование обработки входных данных от Simulink filename = fullfile(basePath, 'app_io.c'); section = '// USER APP INPUT'; tool = ['Работа с буффером для портов S-Function' newline newline ... @@ -127,7 +177,9 @@ classdef appWrap 'app_variable_2 = ReadInputArray(0, 1);' newline newline... '// запись в буфер выходов' newline ... 'app_variable_2 = Buffer[10];']; + elseif strcmp(sel, 'App Outputs') + % Редактирование формирования выходных данных для Simulink filename = fullfile(basePath, 'app_io.c'); section = '// USER APP OUTPUT'; tool = ['Работа с буффером для портов S-Function' newline newline ... @@ -138,17 +190,20 @@ classdef appWrap '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 + % Неизвестный тип кода - выводим сообщение в консоль MATLAB tool = ''; mcuMask.disp(0, '\nОшибка выбора типа секции кода: неизвестное значение'); end - end end end \ No newline at end of file diff --git a/McuLib/m/asynchManage.m b/McuLib/m/asynchManage.m index 71f1834..157f30f 100644 --- a/McuLib/m/asynchManage.m +++ b/McuLib/m/asynchManage.m @@ -1,103 +1,124 @@ classdef asynchManage < handle + % Менеджер асинхронных операций для работы с моделью Simulink + % Решает проблемы блокировки модели при одновременном доступе из GUI + % Использует таймеры для отложенного выполнения операций с моделью + properties (Access = private) - modelName % Имя модели - maskBlockPath % Полный путь к блоку с маской - timerSave - timerUpdate - timerConfigUpdate + modelName % Имя модели Simulink для управления + maskBlockPath % Полный путь к блоку с маской (если нужен доступ к GUI) + timerSave % Таймер для операции сохранения модели + timerUpdate % Таймер для операции обновления модели + timerConfigUpdate % Таймер для обновления конфигурации end methods function obj = asynchManage(modelName, maskBlockPath) - % Конструктор принимает имя модели и путь к блоку с маской + % Конструктор - создает менеджер для асинхронных операций + % modelName - имя модели Simulink для управления + % maskBlockPath - путь к блоку с маской (опционально, для GUI) obj.modelName = modelName; if nargin < 2 - obj.maskBlockPath = ''; % если не передали, оставляем пустым + obj.maskBlockPath = ''; % Если не передали - работаем без GUI else obj.maskBlockPath = maskBlockPath; end end function saveAndUpdateModel(obj) + % Асинхронное сохранение и обновление модели + % Использует таймер чтобы избежать конфликтов доступа к модели obj.timerSave = timer(... - 'StartDelay', 0.01, ... - 'ExecutionMode', 'singleShot', ... - 'TimerFcn', @(~,~) obj.saveCallback()); + 'StartDelay', 0.01, ... % Короткая задержка чтобы освободить модель + 'ExecutionMode', 'singleShot', ... % Однократное выполнение + 'TimerFcn', @(~,~) obj.saveCallback()); % Колбэк сохранения start(obj.timerSave); end - function updateGUIfromConfig(obj) + % Асинхронное обновление GUI из конфигурации + % Используется когда нужно перезагрузить маску с новыми параметрами obj.timerConfigUpdate = timer(... - 'StartDelay', 0.01, ... + 'StartDelay', 0.01, ... % Задержка для стабилизации модели 'ExecutionMode', 'singleShot', ... - 'TimerFcn', @(~,~) obj.GUIconfigCallback()); + 'TimerFcn', @(~,~) obj.GUIconfigCallback()); % Колбэк обновления GUI start(obj.timerConfigUpdate); end end methods (Access = private) function saveCallback(obj) + % Колбэк для сохранения модели - вызывается через таймер try + % Закрываем маску чтобы разблокировать модель для сохранения mcuMask.close(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, ... + 'StartDelay', 0.05, ... % Большая задержка для полного сохранения 'ExecutionMode', 'singleShot', ... - 'TimerFcn', @(~,~) obj.updateCallback()); + '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); + mcuMask.open(obj.maskBlockPath, 1); % Открываем маску снова catch ME warning('progr:Nneg', 'Не удалось открыть маску: %s', ME.message); end end + % Очищаем таймер обновления stop(obj.timerUpdate); delete(obj.timerUpdate); obj.timerUpdate = []; end + function GUIconfigCallback(obj) - + % Колбэк для обновления GUI из конфигурации try + % Закрываем маску и выполняем настройку (mexing) mcuMask.close(obj.maskBlockPath); - mexing(0); + mexing(0); % Функция настройки/компиляции catch ME warning('progr:Nneg', 'Ошибка при обновлении модели: %s', ME.message); end - % Открываем маску, если задан путь к блоку + % Переоткрываем маску с обновленными параметрами if ~isempty(obj.maskBlockPath) try - mcuMask.open(obj.maskBlockPath, 1); + 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 +end \ No newline at end of file diff --git a/McuLib/m/compiler.m b/McuLib/m/compiler.m index 2a0f39f..f7d4732 100644 --- a/McuLib/m/compiler.m +++ b/McuLib/m/compiler.m @@ -1,138 +1,160 @@ classdef compiler + % Класс для компиляции бинарника S-Function в MATLAB + % Управляет процессом сборки mex-файлов для Simulink methods(Static) function compile() + % Основная функция компиляции S-Function + % Добавляет путь к файлам обёртки и запускает компиляцию addpath(mcuPath.get('wrapperPath')); - mexing(1); + mexing(1); % 1 - флаг компиляции end + % хз че это function get_availbe() addpath(mcuPath.get('wrapperPath')); mexing(1); end - function choose() addpath(mcuPath.get('wrapperPath')); mexing(1); end - - function updateRunBat() + % Обновление BAT-файла для компиляции S-Function + % Формирует списки исходных файлов и путей для включения + + % === КОМПИЛЯЦИЯ ОСНОВНОЙ ОБЁРТКИ === sources = { 'MCU.c' 'mcu_wrapper.c' - }; - % Список заголовочных файлов (.h) - includes = { '.\' }; + % Список заголовочных файлов (.h) + includes = { '.\' }; wrapperPath = mcuPath.get('wrapperPath'); - % [wrapperPath, ~, ~] = fileparts(wrapperPath); - % Формируем строки + + % Формируем строки для BAT-файла wrapperSrcText = compiler.createSourcesBat('code_WRAPPER', sources, wrapperPath); wrapperIncText = compiler.createIncludesBat('includes_WRAPPER', includes, wrapperPath); - % Записываем результат - res = compiler.updateRunMexBat(wrapperSrcText, wrapperIncText, ':: WRAPPER BAT'); % Всё прошло успешно - + % Обновляем BAT-файл для основной обёртки + res = compiler.updateRunMexBat(wrapperSrcText, wrapperIncText, ':: WRAPPER BAT'); + + % Если ошибка - прекращаем выполнение if res == 0 return end + % === КОМПИЛЯЦИЯ ПРИЛОЖЕНИЯ ОБЁРТКИ === sources = { 'app_wrapper.c' 'app_init.c' 'app_io.c' }; % Список заголовочных файлов (.h) - includes = { '.\' - }; + includes = { '.\' }; periphPath = mcuPath.get('appWrapperPath'); - % Формируем строки + + % Формируем строки для BAT-файла wrapperSrcText = compiler.createSourcesBat('code_APP_WRAPPER', sources, periphPath); wrapperIncText = compiler.createIncludesBat('includes_APP_WRAPPER', includes, periphPath); - % Записываем результат - res = compiler.updateRunMexBat(wrapperSrcText, wrapperIncText, ':: APP WRAPPER BAT'); % Всё прошло успешно + % Обновляем BAT-файл для обёртки приложения + res = compiler.updateRunMexBat(wrapperSrcText, wrapperIncText, ':: APP WRAPPER BAT'); + % Обновляем BAT-файл для периферии periphConfig.updatePeriphRunMexBat(); end - function res = updateRunMexBat(srcText, incText, Section) + % Обновляет секцию в BAT-файле для компиляции mex % Входные параметры: - % srcText - текст для записи set code_... - % incText - текст для записи set includes_... + % srcText - текст для записи set code_... (исходные файлы) + % incText - текст для записи set includes_... (пути включения) + % Section - секция в BAT-файле для обновления % % Возвращает: - % res - 0 при успехе, 1 при ошибке + % res - 1 при успехе, 0 при ошибке + % Объединяем исходные файлы и пути включения periphBat = [srcText '\n\n' incText]; batPath = fullfile(mcuPath.get('wrapperPath'), 'run_mex.bat'); - res = 1; + res = 1; % По умолчанию - ошибка + try + % Читаем текущее содержимое BAT-файла code = fileread(batPath); - code = regexprep(code, '\r\n?', '\n'); + code = regexprep(code, '\r\n?', '\n'); % Нормализуем переводы строк - % Записываем строки srcText и incText с переносами строк + % Вставляем новую секцию в BAT-файл code = editCode.insertSection(code, Section, periphBat); - fid = fopen(batPath, 'w', 'n', 'UTF-8'); + % Записываем обновленный BAT-файл + fid = fopen(batPath, 'w', 'n'); if fid == -1 error('Не удалось открыть файл для записи'); end fwrite(fid, code); fclose(fid); - res = 1; + res = 1; % Успех catch ME + % Ошибка записи в BAT-файл mcuMask.disp(0, '\nОшибка: неудачная запись в файл при записи файла: %s', ME.message); end end function srcText = createSourcesBat(prefix_name, sources, path) + % Создает строку с исходными файлами для BAT-файла + % Формат: set code_WRAPPER=file1.c file2.c ... + srcList = {}; if nargin >= 2 && iscell(sources) for i = 1:numel(sources) + % Формируем полный путь к исходному файлу fullPath = fullfile(path, sources{i}); - srcList{end+1} = strrep(fullPath, '\', '\\'); + srcList{end+1} = strrep(fullPath, '\', '\\'); % Экранирование слешей end end - % Формируем srcText с переносами строк и ^ + % Формируем srcText с переносами строк и символом продолжения ^ srcText = ''; for i = 1:numel(srcList) if i < numel(srcList) - srcText = [srcText srcList{i} '^' newline ' ']; + srcText = [srcText srcList{i} '^' newline ' ']; % ^ для продолжения строки в BAT else - srcText = [srcText srcList{i}]; + srcText = [srcText srcList{i}]; % Последний файл без продолжения end end - % Добавляем префикс + % Добавляем префикс переменной BAT srcText = ['set ' prefix_name '=' srcText]; end function incText = createIncludesBat(prefix_name, includes, path) + % Создает строку с путями включения для BAT-файла + % Формат: set includes_WRAPPER=-I"path1" -I"path2" ... + incList = {}; if nargin >= 2 && iscell(includes) for i = 1:numel(includes) + % Формируем полный путь для включения fullPath = fullfile(path, includes{i}); - incList{end+1} = ['-I"' strrep(fullPath, '\', '\\') '"']; + incList{end+1} = ['-I"' strrep(fullPath, '\', '\\') '"']; % Флаг include для компилятора end end - % Формируем incText с переносами строк и ^ + % Формируем incText с переносами строк и символом продолжения ^ incText = ''; for i = 1:numel(incList) if i == 1 && numel(incList) ~= 1 - incText = [incText incList{i} '^' newline]; + incText = [incText incList{i} '^' newline]; % Первый элемент с продолжением elseif i < numel(incList) - incText = [incText ' ' incList{i} '^' newline]; + incText = [incText ' ' incList{i} '^' newline]; % Средние элементы с продолжением else - incText = [incText ' ' incList{i}]; + incText = [incText ' ' incList{i}]; % Последний элемент без продолжения end end - % Добавляем префикс + % Добавляем префикс переменной BAT incText = ['set ' prefix_name '=' incText]; end diff --git a/McuLib/m/configJs.m b/McuLib/m/configJs.m index f2cde98..7e7c12f 100644 --- a/McuLib/m/configJs.m +++ b/McuLib/m/configJs.m @@ -1,48 +1,61 @@ classdef configJs + % Класс для работы с JSON конфигурацией периферии в масках Simulink + % Обеспечивает чтение, запись и обновление конфигов из/в JSON файлы methods(Static) function config = update(blockPath, config) + % Обновляет конфигурацию значениями из маски Simulink + % 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}; - % Проверяем есть ли Defines + % Проверяем есть ли секция Defines в этой периферии if ~isfield(config.(periph), 'Defines') continue; end + % Получаем все определения (defines) для этой периферии defines = config.(periph).Defines; defNames = fieldnames(defines); + % Обходим все определения и обновляем их значения из маски for j = 1:numel(defNames) defPrompt = defNames{j}; - paramName = matlab.lang.makeValidName(defPrompt); + paramName = matlab.lang.makeValidName(defPrompt); % Создаем валидное имя параметра - % Проверка, существует ли параметр с таким именем + % Проверяем существует ли параметр с таким именем в маске if ismember(paramName, paramNames) param = mask.getParameter(paramName); - valStr = param.Value; + valStr = param.Value; % Значение как строка из маски - % Проверяем, существует ли элемент defPrompt в структуре defines + % Проверяем существует ли элемент defPrompt в структуре defines if isfield(defines, defPrompt) - % Преобразуем строку в соответствующий тип + % Преобразуем строку из маски в соответствующий тип данных if strcmpi(defines.(defPrompt).Type, 'checkbox') + % Для чекбоксов преобразуем 'on'/'off' в true/false 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 @@ -53,12 +66,18 @@ classdef configJs end function config = read(blockPath) + % Читает JSON конфигурацию из файла указанного в маске + % blockPath - путь к блоку с маской + % Возвращает структуру конфигурации или пустой массив + mask = Simulink.Mask.get(blockPath); + % Получаем путь к конфигурационному файлу из параметра маски pathparam = mask.getParameter('periphPath'); config_path = pathparam.Value; if ~isempty(config_path) + % Читаем и декодируем JSON файл jsonText = fileread(config_path); config = jsondecode(jsonText); else @@ -67,18 +86,26 @@ classdef configJs end function write(config) + % Записывает конфигурацию обратно в JSON файл + % config - структура конфигурации для записи + if isempty(config) return end + % Получаем handle текущего блока и его маску blockHandle = gcbh; mask = Simulink.Mask.get(blockHandle); + % Получаем путь к конфигурационному файлу из маски pathparam = mask.getParameter('periphPath'); config_path = pathparam.Value; + % Кодируем структуру в JSON с красивым форматированием jsonText = jsonencode(config, 'PrettyPrint', true); - fid = fopen(config_path, 'w', 'n', 'UTF-8'); + + % Записываем JSON в файл + fid = fopen(config_path, 'w', 'n'); if fid == -1 error('Не удалось открыть файл periph_config.json для записи.'); end @@ -86,58 +113,65 @@ classdef configJs fclose(fid); end - - function value = get_field(configStruct, targetConfig) - % получить targetConfig структуру из конфига (для глубоко вложенных) + % Рекурсивно ищет поле в структуре конфигурации + % configStruct - структура для поиска + % targetConfig - имя целевого поля + % Возвращает значение поля или пустой массив если не найдено + value = []; fields = fieldnames(configStruct); for i = 1:numel(fields) key = fields{i}; if strcmp(key, targetConfig) + % Нашли прямое совпадение value = configStruct.(key); - return; % нашли и возвращаем + return; elseif isstruct(configStruct.(key)) + % Рекурсивно ищем во вложенной структуре value = configJs.get_field(configStruct.(key), targetConfig); if ~isempty(value) - return; % нашли во вложенной структуре + return; % Нашли во вложенной структуре end end end - % Если не нашли, можно выбросить ошибку или вернуть пустое + % Если не нашли - возвращаем пустой массив if isempty(value) + % Можно раскомментировать для отладки: % error('Поле "%s" не найдено в структуре.', targetConfig); end end function short = get_final_name_from_prefix(prefix) - % Берёт последнее имя после "_" (читаемое имя) + % Извлекает короткое имя из префикса (последняя часть после '_') + % prefix - строка с префиксом вида 'PREFIX_NAME' + % Возвращает последнюю часть после последнего '_' + parts = strsplit(prefix, '_'); - short = parts{end}; + short = parts{end}; % Берём последний элемент end function value = convert_code_value(codeField) - % Преобразует значение поля Options в строку + % Преобразует значение поля Options в строку для отображения + % Обрабатывает разные типы данных: char, string, cell array + if ischar(codeField) value = codeField; elseif isstring(codeField) value = char(codeField); elseif iscell(codeField) + % Объединяем ячейки в одну строку с переносами value = strjoin(codeField, newline); else - % warning('Неподдерживаемый тип данных: сохранено как пустая строка'); + % Для неподдерживаемых типов возвращаем пустую строку value = ''; end end - - end - - methods(Static, Access=private) - + % Приватные методы могут быть добавлены здесь при необходимости end -end +end \ No newline at end of file diff --git a/McuLib/m/customtable.m b/McuLib/m/customtable.m index 7915168..87516b0 100644 --- a/McuLib/m/customtable.m +++ b/McuLib/m/customtable.m @@ -1,159 +1,182 @@ classdef customtable + % Класс для работы с таблицами в масках Simulink + % Обеспечивает форматирование, парсинг и управление табличными данными + % 1. Таблицы в масках Simulink хранятся как строки специального формата: + % {'строка1';'строка2';'строка3'} + % 2. Этот класс преобразует строковое представление в cell-массивы для удобной работы + % 3. Обеспечивает очистку от пустых строк, форматирование колонок и массовое обновление methods(Static) - % формирование таблицы на всю ширину function format(table_name) + % Форматирует таблицу в маске - настраивает колонки и внешний вид + % table_name - имя табличного параметра в маске + block = gcb; mask = Simulink.Mask.get(block); - tableControl = mask.getDialogControl(table_name); - tableParameter = mask.getParameter(table_name); + tableControl = mask.getDialogControl(table_name); % Элемент управления таблицей + tableParameter = mask.getParameter(table_name); % Параметр с данными таблицы nCols = tableControl.getNumberOfColumns; - % инициализация колонок если они пустые - % такое случается при removeParameter + + % Инициализация колонок если они пустые (после removeParameter) if isempty(tableControl.Columns) || (nCols > 1) - for i = 1:nCols - tableControl.removeColumn(1); - end + % Удаляем все существующие колонки + for i = 1:nCols + tableControl.removeColumn(1); + end + % Создаем одну колонку по умолчанию column = tableControl.addColumn(Name='Title', Type='edit'); - tableControl.Sortable = 'on'; + tableControl.Sortable = 'on'; % Разрешаем сортировку end + % Устанавливаем имя колонки равным алиасу параметра column.Name = tableParameter.Alias; end function update(tableName) + % Обновляет таблицу - удаляет пустые строки и сохраняет изменения + % 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; + Table.Value = newStr; % Сохраняем обратно в параметр end end function column_titles = save_all_tables(table_names) - % Очищает столбцы в каждой таблице из массива имен table_names - % Возвращает cell-массив с названиями первых столбцов каждой таблицы - block = gcb; + % Сохраняет и очищает несколько таблиц одновременно + % 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} = ''; + column_titles{k} = ''; % Если колонок нет end end end function restore_all_tables(table_names, column_titles) - % Восстанавливает первый столбец в каждой таблице из массива имен - % Использует массив column_titles для установки имени столбца - block = gcb; + % Восстанавливает таблицы с сохраненными названиями колонок + % table_names - имена таблиц для восстановления + % 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) + % Парсит содержимое таблицы из строкового параметра в cell-массив + % tableName - имя табличного параметра + % Возвращает cell-массив со строками таблицы + block = gcb; - TableStr = get_param(block, tableName); - out = customtable.parse__(TableStr); + TableStr = get_param(block, tableName); % Получаем строковое представление + out = customtable.parse__(TableStr); % Парсим во внутреннем методе end function collect(tableName, cellArray) + % Сохраняет cell-массив обратно в табличный параметр + % tableName - имя параметра для сохранения + % cellArray - cell-массив с данными таблицы + block = gcb; - newTableStr = customtable.collect__(cellArray); - % Записываем обратно в параметр маски - set_param(block, tableName, newTableStr); + newTableStr = customtable.collect__(cellArray); % Конвертируем в строку + set_param(block, tableName, newTableStr); % Сохраняем в параметр end end - - - - methods(Static, Access=private) - function out = parse__(tableStr) + % Внутренний метод для парсинга строки таблицы в cell-массив + % tableStr - строковое представление таблицы в формате {'str1';'str2'} + 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) + % Внутренний метод для конвертации cell-массива в строку таблицы + % cellArray - входной cell-массив + % Возвращает строку в формате {'str1';'str2'} + + % Обрамляем каждую строку в кавычки quoted = cellfun(@(s) ['''' s ''''], cellArray, 'UniformOutput', false); - tableStr = ['{' strjoin(quoted, ';') '}']; + tableStr = ['{' strjoin(quoted, ';') '}']; % Объединяем в формат таблицы end - function cleaned = removeEmptyRows(cellArray) + % Удаляет пустые строки из cell-массива + % cellArray - входной массив данных таблицы + % Возвращает очищенный массив без пустых строк + if isempty(cellArray) cleaned = {}; else - % Проверяем каждую строку, есть ли в ней содержимое (не пустая строка) + % Определяем какие строки пустые (после удаления пробелов) isEmptyRow = cellfun(@(s) isempty(strtrim(s)), cellArray); - cleaned = cellArray(~isEmptyRow); + 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 index c7388ea..918cea1 100644 --- a/McuLib/m/editCode.m +++ b/McuLib/m/editCode.m @@ -1,60 +1,71 @@ classdef editCode + % Класс для редактирования кода C/C++ в текстовых файлах + % Обеспечивает извлечение и вставку кода в секции и функции methods(Static) function newCode = insertSection(code, sectionName, newText) - % insertSection – вставка или замена содержимого секции или тела функции + % Вставка или замена содержимого секции или тела функции + % % Аргументы: - % code – исходный текст (строка) - % sectionName – имя секции (например, 'MY_SECTION') или заголовок функции (например, 'void myFunc(...)') - % newText – новый текст, который будет вставлен внутрь секции или функции + % code - исходный текст кода как строка + % sectionName - имя секции (например, 'MY_SECTION') или + % заголовок функции (например, 'void myFunc(...)') + % newText - новый текст для вставки внутрь секции или функции % % Возвращает: - % newCode – обновлённый текст с подставленным содержимым + % newCode - обновлённый текст с подставленным содержимым % % Особенности: - % - Если sectionName начинается с 'void ', считается что это функция, и вставка происходит внутрь её тела. - % - В остальных случаях ищется секция между маркерами " START" и " END", и она заменяется на newText. + % - Если sectionName начинается с 'void ', считается что это функция, + % и вставка происходит внутрь её тела + % - Для секций ищется блок между маркерами "NAME START" и "NAME END" + % - Поддерживает функции с параметрами и вложенными скобками newCode = code; - % Если это функция + % Обработка функций (если sectionName начинается с 'void ') if startsWith(strtrim(sectionName), 'void ') + % Извлекаем имя функции из заголовка tokens = regexp(sectionName, 'void\s+(\w+)\s*\(', 'tokens'); if isempty(tokens) - return; + return; % Не удалось извлечь имя функции end funcName = tokens{1}{1}; + + % Ищем начало функции в коде expr = sprintf('void\\s+%s\\s*\\(.*?\\)\\s*\\{', funcName); startIdx = regexp(code, expr, 'start'); if isempty(startIdx) - return; + return; % Функция не найдена end - % Найдём тело функции с учётом вложенных скобок + % Находим тело функции с учётом вложенных фигурных скобок from = startIdx(1); - braceCount = 0; + braceCount = 0; % Счётчик уровня вложенности скобок i = from; while i <= length(code) if code(i) == '{' braceCount = braceCount + 1; if braceCount == 1 - bodyStart = i + 1; + bodyStart = i + 1; % Начало тела функции (после {) end elseif code(i) == '}' braceCount = braceCount - 1; if braceCount == 0 - bodyEnd = i - 1; + bodyEnd = i - 1; % Конец тела функции (перед }) break; end end i = i + 1; end + % Проверяем что все скобки закрыты if braceCount ~= 0 - return; + return; % Несбалансированные скобки end + % Собираем новый код: начало до тела + новый текст + конец после тела newCode = [ ... code(1:bodyStart-1), ... newline, newText, newline, ... @@ -63,111 +74,113 @@ classdef editCode 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 - % Формируем новую секцию с нужным текстом - % newText = strrep(newText, '\', '\\'); % экранируем для корректной вставки + % Формируем новую секцию с переданным текстом 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(...)') + % code - исходный текст кода как строка + % sectionName - имя секции (например, 'MY_SECTION') или + % заголовок функции (например, 'void myFunc(...)') % % Возвращает: - % result – извлечённый текст из секции или тела функции + % result - извлечённый текст из секции или тела функции % % Особенности: - % - Если sectionName начинается с 'void ', считается что это функция, и извлекается содержимое её тела. - % - В остальных случаях ищется секция между маркерами " START" и " END". + % - Для функций извлекает содержимое между { и } с учётом вложенности + % - Для секций извлекает текст между маркерами START и END + % - Сохращает оригинальные отступы и форматирование result = ''; - % Если это функция (начинается с 'void ') + + % Обработка функций (если sectionName начинается с 'void ') if startsWith(strtrim(sectionName), 'void ') - % Получаем имя функции из заголовка + % Извлекаем имя функции из заголовка tokens = regexp(sectionName, 'void\s+(\w+)\s*\(', 'tokens'); if isempty(tokens) - return; + return; % Не удалось извлечь имя функции end funcName = tokens{1}{1}; - % Строим шаблон начала функции + % Ищем начало функции в коде expr = sprintf('void\\s+%s\\s*\\(.*?\\)\\s*\\{', funcName); startIdx = regexp(code, expr, 'start'); if isempty(startIdx) - return; + return; % Функция не найдена end - % Поиск тела функции с учётом вложенных скобок + % Находим тело функции с учётом вложенных фигурных скобок from = startIdx(1); - braceCount = 0; + braceCount = 0; % Счётчик уровня вложенности скобок i = from; while i <= length(code) if code(i) == '{' braceCount = braceCount + 1; if braceCount == 1 - % Найдём первую новую строку после { - braceOpenIdx = i; + braceOpenIdx = i; % Позиция открывающей скобки end elseif code(i) == '}' braceCount = braceCount - 1; if braceCount == 0 - braceCloseIdx = i; + braceCloseIdx = i; % Позиция закрывающей скобки break; end end i = i + 1; end + % Проверяем что все скобки закрыты if braceCount ~= 0 - return; + return; % Несбалансированные скобки end - % Найдём \n после { + % Находим первую новую строку после открывающей скобки { newlineAfterOpen = regexp(code(braceOpenIdx:end), '\n', 'once'); if isempty(newlineAfterOpen) - return; + return; % Не найден перевод строки end - bodyStart = braceOpenIdx + newlineAfterOpen; + bodyStart = braceOpenIdx + newlineAfterOpen; % Начало тела функции - % Найдём \n до } + % Находим последнюю новую строку перед закрывающей скобкой } newlinesBeforeClose = regexp(code(1:braceCloseIdx), '\n'); if isempty(newlinesBeforeClose) - return; + return; % Не найдены переводы строк end - bodyEnd = newlinesBeforeClose(end) - 1; + bodyEnd = newlinesBeforeClose(end) - 1; % Конец тела функции - % Извлекаем блок как есть, включая отступы + % Извлекаем тело функции с сохранением форматирования result = code(bodyStart:bodyEnd); return; end - % Иначе считаем, что это секция вида // NAME START ... // NAME END + % Обработка обычных секций (не функций) + % Ищем секцию между маркерами START и 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}; + 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/installTemplates.m b/McuLib/m/installTemplates.m index 2f775bf..7eed166 100644 --- a/McuLib/m/installTemplates.m +++ b/McuLib/m/installTemplates.m @@ -1,5 +1,5 @@ function installTemplates(forceCopy) -% installTemplates Копирует содержимое папки templates (включая вложенные папки) из уровня выше McuLib.slx в текущую папку +% installTemplates Копирует содержимое папки templates (включая вложенные папки) из библиотеки (McuLib.slx) в текущую папку % % installTemplates(forceCopy) % diff --git a/McuLib/m/mainConfig.m b/McuLib/m/mainConfig.m index a588212..f429b4f 100644 --- a/McuLib/m/mainConfig.m +++ b/McuLib/m/mainConfig.m @@ -1,12 +1,20 @@ classdef mainConfig + % Класс для экспорта и импорта конфигурации маски Simulink в JSON + % Позволяет сохранять и загружать настройки блока между сессиями methods(Static) function config = export() + % Экспортирует текущую конфигурацию маски в JSON файл + % Сохраняет параметры обёртки, портов и приложения + blockPath = gcb; mask = Simulink.Mask.get(blockPath); + + % Списки параметров для экспорта по категориям wrapParamToExport = {'wrapperPath', 'enableDebug', 'mcuClk', ... - 'threadCycles', 'enableThreading', 'enableDeinit'}; + 'threadCycles', 'enableThreading', 'enableDeinit', ... + 'periphPath'}; portParamToExport = {'inNumb', ... 'in_port_1_name', 'in_port_1_width', ... 'in_port_2_name', 'in_port_2_width', ... @@ -21,29 +29,32 @@ classdef mainConfig 'out_port_5_name', 'out_port_5_width',}; appParamToExport = {'appWrapperPath', 'srcTable', 'incTable', 'userDefs'}; + % Создаем структуру для конфигурации config = struct(); + + % Экспортируем параметры обёртки for i = 1:numel(wrapParamToExport) paramName = wrapParamToExport{i}; def = mainConfig.exportParamToConfig(mask, paramName); config.(paramName) = def; end + % Экспортируем параметры портов for i = 1:numel(portParamToExport) paramName = portParamToExport{i}; def = mainConfig.exportParamToConfig(mask, paramName); config.(paramName) = def; end + % Экспортируем параметры приложения for i = 1:numel(appParamToExport) paramName = appParamToExport{i}; def = mainConfig.exportParamToConfig(mask, paramName); config.(paramName) = def; end - - - jsonStr = jsonencode(config, 'PrettyPrint', true); % 'PrettyPrint' для форматирования - + % Кодируем структуру в JSON с форматированием + jsonStr = jsonencode(config, 'PrettyPrint', true); % Диалог сохранения файла [file, path] = uiputfile('*.json', 'Сохранить конфигурацию как', 'WrapperConfig.json'); @@ -54,8 +65,6 @@ classdef mainConfig filepath = fullfile(path, file); - - % Сохраняем в файл fid = fopen(filepath, 'w'); if fid == -1 @@ -66,8 +75,10 @@ classdef mainConfig fclose(fid); end - function import() + % Импортирует конфигурацию маски из JSON файла + % Восстанавливает параметры обёртки, портов и приложения + % Получаем путь к текущему блоку blockPath = gcb; mask = Simulink.Mask.get(blockPath); @@ -103,18 +114,21 @@ classdef mainConfig end mcuMask.disp(0, 'Конфигурация успешно импортирована.'); + + % Обновляем периферию после импорта + mcuMask.periphUpdate(); end end - methods(Static, Access=private) - function def = exportParamToConfig(mask, paramName) - % mask — объект Simulink.Mask.get(blockPath) - % paramName — имя параметра (как в mask.Parameters.Name) - + % Экспортирует один параметр маски в структуру для JSON + % mask - объект маски Simulink + % paramName - имя параметра для экспорта + % Возвращает структуру с описанием параметра + param = mask.getParameter(paramName); if isempty(param) mcuMask.disp(0, 'Параметр "%s" не найден в маске.', paramName); @@ -124,53 +138,50 @@ classdef mainConfig def = struct(); - % Prompt + % Сохраняем подпись параметра def.Prompt = param.Prompt; - % Тип параметра + % Сохраняем тип параметра def.Type = param.Type; - % Значение по умолчанию + % Сохраняем значение в зависимости от типа val = param.Value; switch lower(param.Type) case 'checkbox' + % Для чекбоксов преобразуем 'on'/'off' в true/false def.Default = strcmp(val, 'on'); case {'edit', 'spinbox'} + % Для числовых полей пробуем преобразовать в число num = str2double(val); if ~isnan(num) def.Default = num; else - def.Default = val; + def.Default = val; % Оставляем строкой если не число end case 'customtable' - def.Default = customtable.parse(param.Name); % или можно попытаться распарсить значение позже + % Для таблиц парсим содержимое в cell-массив + def.Default = customtable.parse(param.Name); case 'text' - def.Default = ''; % или можно попытаться распарсить значение позже + % Для текстовых полей сохраняем пустую строку + def.Default = ''; otherwise + % Для остальных типов сохраняем как есть def.Default = val; end - % Alias, если есть + % Сохраняем алиас если есть if ~isempty(param.Alias) def.Def = param.Alias; end - - % % Row (new/current) - % try - % rowSetting = param.DialogControl.Row; - % def.NewRow = strcmp(rowSetting, 'new'); - % catch - % def.NewRow = false; - % end end - function applyDefToMask(mask, paramName, def) - % mask — объект Simulink.Mask.get(blockPath) - % paramName — имя параметра маски - % def — структура, полученная из extractDefFromMask - - % Получаем параметр + % Применяет параметр из конфигурации к маске + % mask - объект маски Simulink + % paramName - имя параметра для применения + % def - структура с данными параметра + + % Получаем параметр из маски param = mask.getParameter(paramName); if isempty(param) mcuMask.disp(0, 'Параметр "%s" не найден в маске.', paramName); @@ -182,9 +193,10 @@ classdef mainConfig return; end + % Устанавливаем значение в зависимости от типа параметра switch lower(def.Type) case 'checkbox' - % Логическое значение true/false + % Устанавливаем состояние чекбокса if islogical(def.Default) param.Value = mainConfig.ternary(def.Default, 'on', 'off'); else @@ -192,7 +204,7 @@ classdef mainConfig end case {'edit', 'spinbox'} - % Строка или число + % Устанавливаем значение для текстового поля if isnumeric(def.Default) param.Value = num2str(def.Default); elseif ischar(def.Default) || isstring(def.Default) @@ -202,7 +214,7 @@ classdef mainConfig end case 'customtable' - % Массив строк + % Загружаем данные в таблицу if iscell(def.Default) customtable.collect(paramName, def.Default); else @@ -210,7 +222,7 @@ classdef mainConfig end case 'text' - % Просто текстовая строка + % Устанавливаем текстовое значение if ischar(def.Default) || isstring(def.Default) param.Value = char(def.Default); else @@ -218,7 +230,7 @@ classdef mainConfig end case 'popup' - % popup — установить значение, если оно есть в списке + % Устанавливаем значение выпадающего списка if ischar(def.Default) && isfield(def, 'Def') && any(strcmp(def.Default, def.Def)) param.Value = def.Default; else @@ -226,7 +238,7 @@ classdef mainConfig end otherwise - % По умолчанию просто устанавливаем строковое значение + % Универсальная установка значения if ischar(def.Default) || isstring(def.Default) param.Value = char(def.Default); elseif isnumeric(def.Default) @@ -236,16 +248,16 @@ classdef mainConfig end end - % Применение Prompt, если нужно + % Обновляем подпись параметра если есть if isfield(def, 'Prompt') param.Prompt = def.Prompt; end - % Применение Alias, если есть + % Обновляем алиас если есть if isfield(def, 'Def') && ischar(def.Def) param.Alias = def.Def; end - + % % Установка Row, если поддерживается % if isfield(def, 'NewRow') % try @@ -260,8 +272,9 @@ classdef mainConfig % end end - function result = ternary(cond, a, b) + % Вспомогательная функция - тернарный оператор + % cond - условие, a - значение если true, b - значение если false if cond result = a; else diff --git a/McuLib/m/mcuMask.m b/McuLib/m/mcuMask.m index 0947dcf..438803f 100644 --- a/McuLib/m/mcuMask.m +++ b/McuLib/m/mcuMask.m @@ -1,157 +1,173 @@ classdef mcuMask -%% CALLBACKS + % Главный класс управления маской блока Simulink для микроконтроллера + % Содержит callback-функции и утилиты для работы с маской + +%% CALLBACKS - функции обратного вызова для элементов маски 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); + + % Разрешаем самомодификацию маски и отключаем ссылки set_param(blk,"MaskSelfModifiable","on") set_param(blk, 'LinkStatus', 'none'); - % mcuMask.disp(1,''); + + % Проверяем доступность findjobj для внешней консоли try - % Проверка наличия findjobj findjobjAvailable = exist('findjobj', 'file') == 2; catch findjobjAvailable = false; end - % Имя checkbox-параметра (укажите точное имя из маски) - checkboxParamName = 'extConsol'; % пример - findjobjLinkName = 'findjobj_link'; % пример - % Получаем параметр по имени + + % Настраиваем параметры внешней консоли в зависимости от доступности findjobj + 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 не найден + + % Блокируем чекбокс если findjobj не найден if ~findjobjAvailable checkboxParam.Enabled = 'off'; - checkboxParam.Value = 'off'; % и на всякий случай снимаем галочку + checkboxParam.Value = 'off'; checkboxParam.Prompt = 'External Console (requires findjobj)'; - findjobjLink.Visible = 'on'; + findjobjLink.Visible = 'on'; % Показываем ссылку для скачивания else checkboxParam.Enabled = 'on'; checkboxParam.Prompt = 'External Console'; - findjobjLink.Visible = 'off'; + 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 + %% WRAPPER PARAMS - callback-функции для параметров обёртки function wrapperPath_add(callbackContext) + % Добавление пути к файлам обёртки mcuPath.addPath('wrapperPath'); end function enableThreading(callbackContext) + % Включение/выключение многопоточности mainWrap.enableThreading(); end function enableDeinit(callbackContext) + % Включение/выключение деинициализации mainWrap.enableDeinit(); end function extConsol(callbackContext) + % Управление внешней консолью mainWrap.extConsol(); end - %% USER WRAPPER CODE + %% USER WRAPPER CODE - callback-функции для пользовательского кода function appWrapperPath_add(callbackContext) + % Добавление пути к файлам обёртки приложения mcuPath.addPath('appWrapperPath'); end function appWrapperFunc(callbackContext) + % Загрузка функции обёртки для редактирования appWrap.appWrapperFunc(); end function saveAppWrapperCode(callbackContext) + % Сохранение отредактированного кода обёртки appWrap.saveAppWrapperCode(); end function openAppWrapperCode(callbackContext) + % Открытие кода обёртки во внешнем редакторе appWrap.openAppWrapperCode(); end - %% USER CODE + %% USER CODE - callback-функции для пользовательского кода function srcTable(callbackContext) + % Обновление таблицы исходных файлов customtable.update('srcTable'); end function incTable(callbackContext) + % Обновление таблицы заголовочных файлов customtable.update('incTable'); end function btnAddSrc(callbackContext) + % Добавление исходных файлов через диалог выбора mcuPath.addSourceFileTable('srcTable', 'Выберите исходные файлы'); end function btnAddInc(callbackContext) + % Добавление путей включения через диалог выбора mcuPath.addPathTable('incTable', 'Выберите папку с заголовочными файлами'); end - %% PERIPH CONFIG + %% PERIPH CONFIG - callback-функции для конфигурации периферии function periphPath_add(callbackContext) + % Добавление пути к конфигурационному файлу периферии mcuPath.addAnyFile('periphPath'); end function periphUpdate(callbackContext) - modelName = bdroot(gcb); % получить имя верхнего уровня модели + % Обновление конфигурации периферии с асинхронным управлением + modelName = bdroot(gcb); blockName = gcb; - mgr = asynchManage(modelName, blockName); % создать объект класса - mgr.updateGUIfromConfig(); % запустить сохранение и обновление + mgr = asynchManage(modelName, blockName); + mgr.updateGUIfromConfig(); % Запуск обновления через таймер end - %% COMPILE + %% COMPILE - callback-функции для компиляции function compile(callbackContext) + % Компиляция S-функции compiler.compile(); end function setSFuncName(callbackContext) + % Установка имени S-функции в исходном коде block = gcb; - % Получаем параметр имени S-Function из маски блока newName = mcuMask.get_name(); - % Путь к файлу, в котором надо заменить строку - cFilePath = fullfile(pwd, mcuPath.get('wrapperPath'), 'MCU.c'); % <-- укажи правильный путь + % Путь к файлу MCU.c + cFilePath = fullfile(pwd, mcuPath.get('wrapperPath'), 'MCU.c'); - % Считаем файл в память + % Чтение файла try fileText = fileread(cFilePath); catch return; end - % Регулярное выражение для поиска строки с define - % Заменим строку вида: #define S_FUNCTION_NAME old_name + % Поиск и замена определения имени S-функции 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'); + % Запись изменений обратно в файл + fid = fopen(cFilePath, 'w', 'n'); if fid == -1 error('Не удалось открыть файл для записи.'); end @@ -159,39 +175,33 @@ classdef mcuMask fclose(fid); end - %% LINK TO EXTERNAL CONSOLE + %% LINK TO EXTERNAL CONSOLE - callback для внешней консоли function findjobj_link(callbackContext) - web https://www.mathworks.com/matlabcentral/fileexchange/14317-findjobj-find-java-handles-of-matlab-graphic-objects; + % Открытие ссылки на утилиту findjobj + web('https://www.mathworks.com/matlabcentral/fileexchange/14317-findjobj-find-java-handles-of-matlab-graphic-objects'); end end -%% GENERAL TOOLS +%% GENERAL TOOLS - общие утилиты для работы с маской methods(Static, Access = public) function updateModel() + % Обновление модели с компиляцией S-функции addpath(mcuPath.get('wrapperPath')); res = mexing(1); if res ~= 0 return; end - - % modelName = bdroot(gcb); % получить имя верхнего уровня модели - % blockName = gcb; - % mgr = asynchManage(modelName, blockName); % создать объект класса - % mgr.saveAndUpdateModel(); % запустить сохранение и обновление end function close(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 @@ -201,24 +211,25 @@ classdef mcuMask end function open(blockPath, clear_flag) + % Открытие маски блока open_system(blockPath, 'mask'); mcuMask.disp(clear_flag, ''); end function name = get_name() + % Получение имени S-функции из параметра маски block = gcb; - % Получаем параметр имени S-Function из маски блока name = get_param(block, 'sfuncName'); 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 @@ -226,8 +237,8 @@ classdef mcuMask end end - function children = get_children(ctrl) + % Получение дочерних элементов управления маски if isprop(ctrl, 'DialogControls') children = ctrl.DialogControls; elseif isprop(ctrl, 'Controls') @@ -240,27 +251,28 @@ classdef mcuMask 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 + % Добавление имени параметра + params{end+1} = ctrl.Name; 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); @@ -271,14 +283,9 @@ classdef mcuMask end end - function tool(text, example) - % Устанавливает заданный текст в параметр Text Area 'toolText' через объект маски - - % Получаем ссылку на текущий блок + % Установка текста подсказки и примера в элементы маски block = gcb; - - % Получаем объект маски mask = Simulink.Mask.get(block); toolTextArea = mask.getDialogControl('toolText'); @@ -288,15 +295,14 @@ classdef mcuMask 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 @@ -304,26 +310,27 @@ classdef mcuMask if ~strcmp(out, '') && ~strcmp(out_now, '') set_param(gcb, 'consoleOutput', [out_now newline out]); else - set_param(gcb, 'consoleOutput', [out_now out]); + set_param(gcb, 'consoleOutput', [out_now out]); + end end - 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 - + function v = getMyLibVersion() + % Получение версии библиотеки + v = 'pre-1.04'; + end end end \ No newline at end of file diff --git a/McuLib/m/mcuPath.m b/McuLib/m/mcuPath.m index fefc8b8..273189f 100644 --- a/McuLib/m/mcuPath.m +++ b/McuLib/m/mcuPath.m @@ -1,109 +1,150 @@ classdef mcuPath + % Класс для работы с путями файлов и папок в маске Simulink + % Обеспечивает преобразование путей, добавление в таблицы и параметры + methods(Static) - %% GET PATH FROM PARAM + %% GET PATH FROM PARAM - получение путей из параметров маски function path = get(paramName) + % Получение значения пути из параметра маски + % paramName - имя параметра маски содержащего путь + % Возвращает строку с путём (абсолютным или относительным) + blockPath = gcb; path = get_param(blockPath, paramName); end - %% ADD PATH TO TABLE + %% ADD PATH TO TABLE - добавление путей в табличные параметры function addSourceFileTable(targetParamName, message) - % Открываем проводник для выбора файлов + % Добавление исходных файлов в таблицу через диалог выбора + % targetParamName - имя табличного параметра маски + % message - сообщение в диалоге выбора + + % Открываем проводник для выбора файлов с фильтрами [files, pathstr] = uigetfile({ ... '*.c;*.cpp', 'Исходные файлы (*.c, *.cpp)'; ... '*.obj;*.lib', 'Библиотеки (*.obj, *.lib)'; ... '*.*', 'Все файлы (*.*)'}, ... message, ... - 'MultiSelect', 'on'); + 'MultiSelect', 'on'); % Разрешаем множественный выбор if isequal(files, 0) - return; % Отмена выбора + return; % Пользователь отменил выбор end + + % Нормализуем входные данные: один файл -> cell array if ischar(files) - files = {files}; % Один файл — в cell + files = {files}; end - % Парсим строку в cell-массив + + % Читаем текущее содержимое таблицы oldTable = customtable.parse(targetParamName); % Добавляем новые пути, проверяя уникальность for i = 1:numel(files) fullpath = fullfile(pathstr, files{i}); + % Преобразуем абсолютный путь в относительный rel = mcuPath.absoluteToRelativePath(fullpath); + % Добавляем только если путь ещё не существует в таблице if ~any(strcmp(rel, oldTable)) oldTable{end+1, 1} = rel; end end - % Парсим строку в cell-массив + % Сохраняем обновленную таблицу обратно в параметр маски customtable.collect(targetParamName, oldTable); - end function addPathTable(targetParamName, message) - % Открываем проводник для выбора папок + % Добавление путей к папкам в таблицу через диалог выбора + % targetParamName - имя табличного параметра маски + % message - сообщение в диалоге выбора + + % Открываем проводник для выбора папки pathstr = uigetdir(pwd, message); if isequal(pathstr, 0) - return; % Отмена выбора + return; % Пользователь отменил выбор end - % Парсим таблицу + + % Читаем текущее содержимое таблицы oldTable = customtable.parse(targetParamName); + % Преобразуем абсолютный путь в относительный rel = mcuPath.absoluteToRelativePath(pathstr); - % Проверяем наличие пути + % Проверяем наличие пути в таблице и добавляем если нужно if ~any(strcmp(rel, oldTable)) oldTable{end+1, 1} = rel; end - % Собираем таблицу + % Сохраняем обновленную таблицу customtable.collect(targetParamName, oldTable); end - %% ADD PATH TO EDIT + %% ADD PATH TO EDIT - добавление путей в текстовые параметры function addPath(targetParamName, message) + % Добавление пути к папке в текстовый параметр маски + % targetParamName - имя параметра маски для пути + % message - сообщение в диалоге выбора (не используется) + block = gcb; mask = Simulink.Mask.get(block); + % Открываем окно выбора папки folderPath = uigetdir('', 'Выберите папку'); - % Проверка на отмену + + % Проверка на отмену выбора if isequal(folderPath, 0) return; end - % Установка значения параметра маски + + % Преобразуем абсолютный путь в относительный rel = mcuPath.absoluteToRelativePath(folderPath); + + % Устанавливаем значение параметра маски param = mask.getParameter(targetParamName); param.Value = rel; end function addAnyFile(targetParamName, message) + % Добавление пути к файлу в текстовый параметр маски + % targetParamName - имя параметра маски для пути к файлу + % message - сообщение в диалоге выбора (не используется) + block = gcbh; mask = Simulink.Mask.get(block); + + % Открываем проводник для выбора любого файла [file, path] = uigetfile({'*.*','Все файлы (*.*)'}, 'Выберите файл'); + if isequal(file, 0) || isequal(path, 0) - % Отмена выбора — ничего не делаем - return; + return; % Пользователь отменил выбор end + + % Формируем полный путь и преобразуем в относительный fullFilePath = fullfile(path, file); rel = mcuPath.absoluteToRelativePath(fullFilePath); + + % Устанавливаем значение параметра маски param = mask.getParameter(targetParamName); param.Value = rel; end - %% GET PATH STRING + %% GET PATH STRING - утилиты преобразования путей function absPath = getAbsolutePath(relPath) - % relativeToAbsolutePath — преобразует относительный путь в абсолютный. - % - % Если путь уже абсолютный — возвращается он же, приведённый к канонической форме. - % Если путь относительный — преобразуется относительно текущей директории. + % Преобразование относительного пути в абсолютный + % relPath - относительный или абсолютный путь + % Возвращает абсолютный путь в канонической форме - % Проверка: абсолютный ли путь + % Проверка: абсолютный ли путь уже if ispc + % Для Windows: путь начинается с буквы диска или \\ isAbsolute = ~isempty(regexp(relPath, '^[a-zA-Z]:[\\/]', 'once')) || startsWith(relPath, '\\'); else + % Для Unix: путь начинается с / isAbsolute = startsWith(relPath, '/'); end @@ -111,7 +152,7 @@ classdef mcuPath % Канонизируем абсолютный путь (убираем ./, ../ и т.п.) absPath = char(java.io.File(relPath).getCanonicalPath()); else - % Строим абсолютный путь от текущей директории + % Строим абсолютный путь относительно текущей директории cwd = pwd; combined = fullfile(cwd, relPath); absPath = char(java.io.File(combined).getCanonicalPath()); @@ -119,50 +160,52 @@ classdef mcuPath end function rel = absoluteToRelativePath(pathstr) - % absoluteToRelativePath — преобразует абсолютный путь в относительный от текущей директории. + % Преобразование абсолютного пути в относительный относительно текущей директории + % pathstr - абсолютный путь для преобразования + % Возвращает относительный путь % - % Если путь находится в текущей директории или вложенной в неё — добавляется префикс './' - % Если выше — формируются переходы '..' - % Если путь совпадает с текущей директорией — возвращается '.' - + % Примеры: + % Если путь в текущей директории -> './filename' + % Если путь выше -> '../parent/filename' + % Если путь совпадает с текущей директорией -> '.' + % Получаем текущую рабочую директорию 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 - end end \ No newline at end of file diff --git a/McuLib/m/mcuPorts.m b/McuLib/m/mcuPorts.m index 76770b7..6800fea 100644 --- a/McuLib/m/mcuPorts.m +++ b/McuLib/m/mcuPorts.m @@ -1,45 +1,63 @@ classdef mcuPorts + % Класс для управления портами ввода-вывода в маске Simulink + % Генерирует конфигурационные файлы для портов на основе параметров маски methods(Static) function write() + % Основная функция записи конфигурации портов в файлы + % Генерирует заголовочный файл и файл реализации для портов + block = gcb; mask = Simulink.Mask.get(block); + + % Пути к конфигурационным файлам hPath = fullfile(mcuPath.get('wrapperPath'), 'mcu_wrapper_conf.h'); cPath = fullfile(mcuPath.get('wrapperPath'), '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'); + % Записываем обновленный заголовочный файл + fid = fopen(hPath, 'w', 'n'); 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'); + + % Записываем обновленный файл реализации + fid = fopen(cPath, 'w', 'n'); if fid == -1 error('Не удалось открыть файл для записи'); end @@ -47,39 +65,46 @@ classdef mcuPorts fclose(fid); end - function [portwidth, defnames] = getMaskNames(port_prefix) + % Получение имен и ширин портов из параметров маски + % port_prefix - префикс портов ('in' или 'out') + % Возвращает ширины портов и их имена + 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 @@ -87,84 +112,93 @@ classdef mcuPorts methods(Static, Access=private) - - function updateMask_prefix(port_prefix) + % Обновление видимости параметров для конкретного типа портов + % port_prefix - префикс портов ('in' или 'out') + 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) + % Установка значений по умолчанию для неиспользуемых портов + % port_prefix - префикс портов ('in' или 'out') + 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); + param.Value = upper(port_prefix); % Имя по умолчанию + paramName = sprintf('%s_port_%d_width', port_prefix, i); param = mask.getParameter(paramName); - param.Value = '16'; + param.Value = '16'; % Ширина по умолчанию end end - function headerText = addPortHeaderDefines(existingText, widths, prefixNumb, portPrefixes) - % existingText — структура с полями PARAMS, AUTO_PARAMS - % widths — вектор ширин портов - % prefixNumb — префикс общего счётчика (например, 'OUT') - % portPrefixes — {'OUT', 'OUT', ...} - + % Генерация define-макросов для заголовочного файла + % existingText - существующий текст конфигурации + % widths - вектор ширин портов + % prefixNumb - общий префикс ('IN' или 'OUT') + % portPrefixes - массив префиксов для каждого порта + % Возвращает структуру с PARAMS и AUTO_PARAMS + n = numel(widths); upperPrefix = upper(prefixNumb); - % === PARAMS === + % === БАЗОВЫЕ ПАРАМЕТРЫ === lines = { sprintf('#define %s_PORT_NUMB %d', upperPrefix, n) }; + + % Добавляем define для ширины каждого порта for i = 1:n lines{end+1} = sprintf('#define %s_PORT_%d_WIDTH %d', ... upper(portPrefixes{i}), i, widths(i)); @@ -172,10 +206,10 @@ classdef mcuPorts 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); @@ -188,6 +222,7 @@ classdef mcuPorts lines{end+1} = ''; lines{end+1} = sprintf('/// === Смещения массивов (внутри общего буфера) ==='); + % Генерируем define для смещений каждого массива в буфере for i = 1:n if i == 1 lines{end+1} = sprintf('#define OFFSET_%s_ARRAY_1 0', upperPrefix); @@ -198,7 +233,7 @@ classdef mcuPorts end newAuto = strjoin(lines, newline); - % === Добавление к существующему === + % === ОБЪЕДИНЕНИЕ С СУЩЕСТВУЮЩИМ ТЕКСТОМ === if isfield(existingText, 'PARAMS') headerText.PARAMS = [existingText.PARAMS, newline, newParams]; else @@ -212,40 +247,43 @@ classdef mcuPorts end end - function cText = addPortCDefines(existingText, widths, prefixNumb, portPrefixes) - % existingText — существующий текст .c - % widths — вектор ширин портов - % prefixNumb — общий префикс ('OUT') - % portPrefixes — {'OUT', 'LOG', ...} - + % Генерация кода для файла реализации (.c) + % existingText - существующий текст + % widths - вектор ширин портов + % prefixNumb - общий префикс ('IN' или 'OUT') + % portPrefixes - массив префиксов для каждого порта + % Возвращает текст для вставки в .c файл + 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 @@ -258,6 +296,7 @@ classdef mcuPorts newText = strjoin(lines, newline); + % Объединяем с существующим текстом if nargin < 1 || isempty(existingText) cText = newText; else @@ -265,20 +304,14 @@ classdef mcuPorts end end - - - - function val = iff(cond, a, b) - % Условное выражение inline + % Вспомогательная функция - тернарный оператор + % cond - условие, a - значение если true, b - значение если false 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 index 1b42f89..4524278 100644 --- a/McuLib/m/mexing.m +++ b/McuLib/m/mexing.m @@ -1,146 +1,153 @@ -% Компилирует S-function +% Компилирует S-function для блока микроконтроллера в Simulink +% compile_mode: 1 - компиляция, 0 - обновление конфигурации function res = mexing(compile_mode) global Ts - Ts = 0.00001; + Ts = 0.00001; % Установка глобального времени дискретизации if compile_mode == 1 - delete('*.mexw64') - delete('*.mexw64.pdb') - delete([mcuPath.get('wrapperPath'), '\Outputs\*.*']); - set_param(gcb, 'consoleOutput', ''); - compiler.updateRunBat(); - % Дефайны - 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'); - - Name = mcuMask.get_name(); - - % Вызов батника с двумя параметрами: includes и code - run_bat_mex_path = fullfile(mcuPath.get('wrapperPath'), 'run_mex.bat'); - cmd = sprintf('%s %s "%s" "%s" "%s" "%s" %s %s', run_bat_mex_path, Name, 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); + % === РЕЖИМ КОМПИЛЯЦИИ === + setenv('VSLANG', '1033'); % Английский для Visual Studio + % Обновление параметров блока block = gcb; - newName = get_param(block, 'sfuncName'); oldName = get_param(block, 'FunctionName'); if ~strcmp(newName, oldName) - set_param(block, 'FunctionName', newName); + 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); + set_param(block, 'Parameters', newParam); % Обновление параметров end + + % Очистка предыдущих файлов компиляции + delete('*.mexw64') + delete('*.mexw64.pdb') + delete([mcuPath.get('wrapperPath'), '\Outputs\*.*']); + set_param(gcb, 'consoleOutput', ''); % Очистка консоли вывода + + % Обновление BAT-файла для компиляции + compiler.updateRunBat(); + + % Формирование дефайнов для компиляции + 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'); + Name = mcuMask.get_name(); % Имя S-функции + + % Вызов батника компиляции + run_bat_mex_path = fullfile(mcuPath.get('wrapperPath'), 'run_mex.bat'); + cmd = sprintf('%s %s "%s" "%s" "%s" "%s" %s %s', run_bat_mex_path, Name, includesArg, codeArg, definesUserArg, definesConfigArg, modeArg, echoArg); + + if mcuMask.read_checkbox('extConsol') + % Запуск с внешней консолью + cmdout = runBatAndShowOutput(cmd); + else + % Запуск в фоновом режиме + [status, cmdout]= system(cmd); + end + + % Сохранение вывода в параметр маски + mcuMask.disp(1, cmdout); if status == 0 - res = 0; + res = 0; % Успешная компиляция else - res = 1; + res = 1; % Ошибка компиляции end - beep + beep % Звуковое уведомление + else + % === РЕЖИМ ОБНОВЛЕНИЯ КОНФИГУРАЦИИ === blockPath = gcb; - config = configJs.read(blockPath); - config = configJs.update(blockPath, config); - configJs.write(config); - periphConfig.updateMask(blockPath, config); + config = configJs.read(blockPath); % Чтение конфигурации + config = configJs.update(blockPath, config); % Обновление конфигурации + configJs.write(config); % Запись конфигурации + periphConfig.updateMask(blockPath, config); % Обновление маски end end -%% COMPILE PARAMS - +%% COMPILE PARAMS - функции формирования параметров компиляции function [includesArg, codeArg] = make_mex_arguments(incTableName, srcTableame) -%MAKE_MEX_ARGUMENTS Формирует строки аргументов для вызова mex-компиляции через батник -% -% [includesArg, codeArg] = make_mex_arguments(includesCell, codeCell) +% Формирует строки аргументов для вызова mex-компиляции через батник % % Вход: -% includesCell — ячейковый массив путей к директориям include -% codeCell — ячейковый массив исходных файлов +% incTableName - имя таблицы с путями включения +% srcTableame - имя таблицы с исходными файлами % % Выход: -% includesArg — строка для передачи в батник, например: "-I"inc1" -I"inc2"" -% codeArg — строка с исходниками, например: ""src1.c" "src2.cpp"" +% includesArg - строка с флагами включения (-I"path") +% codeArg - строка с исходными файлами ("file1.c" "file2.cpp") - - % Здесь пример получения из маски текущего блока (замени по своему) + % Получение данных из таблиц маски includesCell = customtable.parse(incTableName); codeCell = customtable.parse(srcTableame); - % Оборачиваем пути в кавычки и добавляем -I + % Формирование строки путей включения с флагом -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}; % Текст с пользовательскими определениями + definesText = maskValues{idxUserDefs}; - % Убираем буквальные символы \n и \r + % Обработка специальных символов definesText = strrep(definesText, '\n', ' '); definesText = strrep(definesText, '\r', ' '); - % Разбиваем по переносам строк + % Разбиение на строки lines = split(definesText, {'\n', '\r\n', '\r'}); - parts = strings(1,0); % пустой массив строк + parts = strings(1,0); % массив для хранения дефайнов for k = 1:numel(lines) line = strtrim(lines{k}); @@ -148,7 +155,7 @@ function definesUserArg = parseDefinesMaskText() continue; end - % Разбиваем по пробелам, чтобы получить отдельные определения в строке + % Разбиение строки на токены tokens = split(line); for t = 1:numel(tokens) @@ -157,11 +164,13 @@ function definesUserArg = parseDefinesMaskText() 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); @@ -172,13 +181,13 @@ function definesUserArg = parseDefinesMaskText() definesUserArg = strjoin(parts, ' '); end - - function definesWrapperArg = buildConfigDefinesString() +% Формирование дефайнов из конфигурации периферии + blockHandle = gcbh; mask = Simulink.Mask.get(blockHandle); - tabName = 'configTabAll'; % Имя вкладки (Prompt) + tabName = 'configTabAll'; % Имя вкладки с конфигурацией tabCtrl = mask.getDialogControl(tabName); @@ -186,17 +195,15 @@ function definesWrapperArg = buildConfigDefinesString() 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); @@ -205,33 +212,32 @@ function definesWrapperArg = buildConfigDefinesString() case 'popup' definesWrapperArg = addDefineByParam(definesWrapperArg, paramName, 0); 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 + param = mask.getParameter(paramName); - % Найдём индекс нужного параметра + % Поиск индекса параметра idxParam = find(strcmp(paramNames, paramName), 1); if isempty(idxParam) error('Parameter "%s" not found in block mask parameters.', paramName); end - % Берём alias из маски + % Определение имени дефайна (алиас или значение) val = ''; if ~strcmp(param.Type, 'popup') def_name = param.Alias; @@ -248,29 +254,32 @@ function definesWrapperArg = addDefineByParam(definesWrapperArg, paramName, val_ return; end + % Формирование дефайна в зависимости от типа параметра if val_define ~= 0 - % Значение параметра + % Параметры с значениями val = maskValues{idxParam}; if strcmp(param.Evaluate, 'on') - val = evalin('base', val); % Вычисляем выражение - val = num2str(val); % Преобразуем результат в строку + val = evalin('base', val); % Вычисление выражений + val = num2str(val); end - % Формируем define с кавычками и значением newDefine = ['-D"' def_name '__EQ__' val '"']; elseif ~strcmp(param.Type, 'popup') + % Чекбоксы if mcuMask.read_checkbox(paramName) - % Формируем define с кавычками без значения newDefine = ['-D"' def_name '"']; else newDefine = ''; end else - newDefine = ['-D"' def_name '__EQ__' val '"']; + % Выпадающие списки + if strcmp(param.Alias, '') + newDefine = ['-D"' def_name '"']; + else + newDefine = ['-D"' def_name '__EQ__' val '"']; + end end - - - % Добавляем новый define к существующему (string) + % Добавление дефайна к результирующей строке if isempty(definesWrapperArg) || strlength(strtrim(definesWrapperArg)) == 0 definesWrapperArg = newDefine; else @@ -278,19 +287,22 @@ function definesWrapperArg = addDefineByParam(definesWrapperArg, paramName, val_ end end +%% CONSOLE FUNCTIONS - функции работы с консолью -%% CONSOLE FUNCTIONS function cmdret = runBatAndShowOutput(cmd) +% Запуск BAT-файла с отображением вывода в реальном времени + import java.io.*; import java.lang.*; - cmdEnglish = ['chcp 437 > nul && ' cmd]; + + 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 = ""; % Здесь будем накапливать весь вывод + cmdret = ""; while true if reader.ready() @@ -298,36 +310,37 @@ function cmdret = runBatAndShowOutput(cmd) if isempty(line) break; end - cmdret = cmdret + string(line) + newline; % сохраняем вывод - % Здесь выводим только новую строку - safeLine = strrep(line, '''', ''''''); % Экранируем апострофы - logWindow_append(safeLine); - drawnow; % обновляем GUI + cmdret = cmdret + string(line) + newline; + safeLine = strrep(line, '''', ''''''); % Экранирование кавычек + logWindow_append(safeLine); % Вывод в окно лога + drawnow; else if ~process.isAlive() - % дочитываем оставшиеся строки + % Дочтение оставшегося вывода while reader.ready() line = char(reader.readLine()); if isempty(line) break; end - cmdret = cmdret + string(line) + newline; % сохраняем вывод + cmdret = cmdret + string(line) + newline; safeLine = strrep(line, '''', ''''''); logWindow_append(safeLine); drawnow; end break; end - pause(0.2); + 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', ... @@ -339,10 +352,12 @@ function logWindow_append(line) 'BackgroundColor', 'white', ... 'Tag', 'LogWindowFigure'); - jScrollPane = findjobj(hEdit); % JScrollPane - jTextArea = jScrollPane.getViewport.getView; % JTextArea внутри JScrollPane + % Получение Java-компонентов для управления скроллингом + jScrollPane = findjobj(hEdit); + jTextArea = jScrollPane.getViewport.getView; end + % Добавление новой строки oldText = get(hEdit, 'String'); if ischar(oldText) oldText = {oldText}; @@ -350,32 +365,30 @@ function logWindow_append(line) 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 + % Получение всех открытых окон Java 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) @@ -383,12 +396,11 @@ function isOpen = isMaskDialogOpen(blockPath) return; end catch - % Окно не имеет заголовка — пропускаем + % Пропуск окон без заголовка end end end catch isOpen = false; end -end - +end \ No newline at end of file diff --git a/McuLib/m/periphConfig.m b/McuLib/m/periphConfig.m index ee6d26e..d89dcf5 100644 --- a/McuLib/m/periphConfig.m +++ b/McuLib/m/periphConfig.m @@ -1,37 +1,45 @@ classdef periphConfig + % Класс для управления конфигурацией периферии в маске Simulink + % Динамически создает элементы маски на основе JSON конфигурации methods(Static) function updateMask(blockPath, config) - % blockPath = [blockPath '/MCU']; - - % Проверяем, была ли маска открыта -% wasOpen = isMaskDialogOpen(blockPath); + % Основная функция обновления маски на основе конфигурации + % Динамически создает вкладки и параметры для периферийных модулей + % blockPath - путь к блоку Simulink + % config - структура конфигурации из JSON файла + mask = Simulink.Mask.get(blockPath); + % Сохраняем состояние таблиц перед изменением маски tableNames = {'incTable', 'srcTable'}; columns_backup = customtable.save_all_tables(tableNames); + try + % Получаем настройки ширины строк из параметра маски rowWidth = str2double(get_param(blockPath, 'rowWidth')); + % Очищаем контейнер конфигурации перед созданием новых элементов containerName = 'configTabAll'; periphConfig.clear_all_from_container(mask, containerName); - % Ищем контейнер, в который будем добавлять вкладки + % Получаем контейнер для вкладок конфигурации container = mask.getDialogControl(containerName); if isempty(container) error('Контейнер "%s" не найден в маске.', containerName); end + % Обрабатываем конфигурацию если она не пустая if ~isempty(config) - % Проходим по каждому модулю (ADC, TIM...) + % Проходим по каждому модулю периферии (ADC, TIM, UART и т.д.) periphs = fieldnames(config); for i = 1:numel(periphs) periph = periphs{i}; - % Сохраняем код, если он есть + % Сохраняем код модуля (исходники и инклюды) в скрытые параметры periphConfig.store_single_periph_code(mask, periph, config.(periph)); - % Проверяем наличие Defines + % Пропускаем модули без секции Defines if ~isfield(config.(periph), 'Defines') continue; end @@ -39,68 +47,78 @@ classdef periphConfig defines = config.(periph).Defines; defNames = fieldnames(defines); - % Создаём вкладку для модуля + % Создаем вкладку для модуля периферии tabCtrl = container.addDialogControl('tab', periph); - tabCtrl.Prompt = [periph ' Config']; + tabCtrl.Prompt = [periph ' Config']; % Отображаемое имя вкладки + % Карта для отслеживания количества строк в каждой вкладке rowCountMap = containers.Map(); + + % Добавляем все параметры Defines в созданную вкладку for j = 1:numel(defNames) defPrompt = defNames{j}; def = defines.(defPrompt); - % Вызов функции добавления одного параметра + % Добавление одного параметра конфигурации periphConfig.addConfig(mask, containerName, periph, defPrompt, def, rowCountMap, rowWidth); end end end + + % Создаем скрытые параметры для хранения кода периферии periphConfig.create_all_code_storage_params(blockPath, config); - % periphConfig.cleanup_obsolete_code_params(blockPath, config); - + + % Обновляем состояние маски (включение/выключение вкладок) periphConfig.update(); - % Восстанавлиperiph = allTabNamesваем таблицы + % Восстанавливаем таблицы после изменений customtable.restore_all_tables(tableNames, columns_backup); catch - % Восстанавливаем таблицы + % В случае ошибки восстанавливаем таблицы customtable.restore_all_tables(tableNames, columns_backup); end - % % Повторно открываем маску, если она была открыта - % if wasOpen - % open_system(blockPath, 'mask'); - % end end function update() + % Обновление состояния маски - включение/выключение вкладок + % на основе состояний чекбоксов управления + blockPath = gcb; mask = Simulink.Mask.get(blockPath); + % Читаем текущую конфигурацию config = configJs.read(blockPath); containerName = 'configTabAll'; container = mask.getDialogControl(containerName); + + % Получаем все параметры для проверки наличия чекбоксов paramsAll = mcuMask.collect_all_parameters(container); - % Получаем все имена в кладок из container (у вас должен быть container) + + % Получаем имена всех вкладок в контейнере allTabs = container.DialogControls; allTabNames = arrayfun(@(t) t.Name, allTabs, 'UniformOutput', false); fieldsConfig = fieldnames(config); - % Цикл по всем вкладкам в контейнере + + % Обрабатываем каждую вкладку for i = 1:length(allTabNames) periph = fieldsConfig{i}; - % Попытка найти параметр чекбокса Tab__Enable + % Проверяем наличие чекбокса управления для этой вкладки checkboxName = ['Tab_' periph '_Enable']; if ismember(checkboxName, paramsAll) - % Чекбокс есть - проверяем его состояние + % Чекбокс найден - управляем состоянием вкладки paramObj = mask.getParameter(checkboxName); val = paramObj.Value; try tab = container.getDialogControl(periph); if strcmpi(val, 'off') + % Выключаем вкладку и очищаем связанные параметры tab.Enabled = 'off'; - % Рекурсивно очищаем связанные скрытые параметры periphConfig.clear_tab_params(mask, config.(periph), periph); else + % Включаем вкладку и синхронизируем параметры tab.Enabled = 'on'; periphConfig.sync_tab_params(mask, config.(periph), periph); end @@ -109,31 +127,32 @@ classdef periphConfig end else - % Чекбокса нет — просто пытаемся включить вкладку, если она есть + % Чекбокса нет - просто включаем вкладку try tab = container.getDialogControl(periph); tab.Enabled = 'on'; - % Опционально можно синхронизировать параметры, если есть config поле + % Синхронизируем параметры если есть конфигурация if isfield(config, periph) - periphConfig.sync_tab_params(mask, config.(periph), periph); + periphConfig.sync_tab_params(mask, config.(periph), periph); end catch - % Можно не выводить предупреждение — вкладка может быть необязательной - % warning('Вкладка с именем "%s" не найдена.', periph); + % Вкладка может быть необязательной - игнорируем ошибку end end end - end function periphParamCallback(paramName) + % Callback-функция для параметров периферии + % Обрабатывает изменения чекбоксов и других параметров + blockPath = gcb; mask = Simulink.Mask.get(blockPath); hObj = mask.getParameter(paramName); config = configJs.read(blockPath); container = mask.getDialogControl('configTabAll'); - % === Проверка на Tab__Enable === + % === Обработка чекбоксов управления вкладками === exprTab = '^Tab_(\w+)_Enable$'; tokensTab = regexp(paramName, exprTab, 'tokens'); @@ -145,11 +164,13 @@ classdef periphConfig paramVal = hObj.Value; if strcmpi(paramVal, 'off') + % Выключаем вкладку и очищаем параметры tab.Enabled = 'off'; if isfield(config, periph) periphConfig.clear_tab_params(mask, config.(periph), periph); end else + % Включаем вкладку и синхронизируем параметры tab.Enabled = 'on'; if isfield(config, periph) periphConfig.sync_tab_params(mask, config.(periph), periph); @@ -161,19 +182,17 @@ classdef periphConfig return; end - % === Проверка на параметр, связанный с Sources/Includes === - % Проверка: это параметр, у которого есть соответствующий Hidden__Sources или Hidden__Includes + % === Обработка параметров, связанных с Sources/Includes === nameBase = paramName; paramNames = string({mask.Parameters.Name}); hasSources = any(paramNames == "Hidden_" + nameBase + "_Sources"); hasIncludes = any(paramNames == "Hidden_" + nameBase + "_Includes"); - if hasSources || hasIncludes useVal = hObj.Value; - % Получаем содержимое config по nameBase — возможно, вложенное + % Получаем структуру конфигурации для этого параметра try valueStruct = configJs.get_field(config, nameBase); catch @@ -182,49 +201,50 @@ classdef periphConfig end if strcmpi(useVal, 'on') + % Сохраняем код если параметр включен try periphConfig.store_single_periph_code(mask, nameBase, valueStruct); catch warning('Не удалось сохранить параметры для %s.', nameBase); end else + % Очищаем код если параметр выключен periphConfig.clear_single_periph_code_param(mask, nameBase); end - return; end - - - % === Если не подошло ни под одно из условий === - % warning('Объект "%s" не поддерживается универсальным коллбеком.', paramName); end function updatePeriphRunMexBat() - % Запись run_mex.bat + % Обновление BAT-файла для компиляции с кодом периферии + % Собирает все исходники и инклюды из скрытых параметров + blockPath = gcb; + % Восстанавливаем код периферии из скрытых параметров маски CodeStruct = periphConfig.restore_periph_code_from_mask(blockPath); periphPath = mcuPath.get('periphPath'); [periphPath, ~, ~] = fileparts(periphPath); + % Добавляем код в BAT-файл компиляции periphConfig.addCodeBat(CodeStruct, periphPath); end - end - - methods(Static, Access=private) - - - function addHiddenParam(mask, containerName, nameBase, kind, existingParams) - % Преобразуем к красивому имени + % Создание скрытого параметра для хранения кода + % nameBase - базовое имя параметра + % kind - тип ('Sources' или 'Includes') + prettyName = strrep(nameBase, '_', ' '); paramName = ['Hidden_' char(nameBase) '_' kind]; + + % Проверяем не существует ли уже параметр if ismember(paramName, existingParams) return; end + % Создаем скрытый параметр mask.addParameter( ... 'Name', paramName, ... 'Type', 'edit', ... @@ -233,14 +253,16 @@ classdef periphConfig 'Visible', 'off', ... 'Container', containerName ... ); - fprintf('Создан скрытый параметр: %s\n', paramName); end function clear_tab_params(mask, configStruct, prefix, depth) + % Рекурсивная очистка параметров вкладки + % Используется при выключении вкладки + if nargin < 4 depth = 0; end - maxDepth = 3; % Максимальная глубина рекурсии + maxDepth = 3; % Ограничение глубины рекурсии fields = fieldnames(configStruct); @@ -251,10 +273,11 @@ classdef periphConfig if isstruct(value) if depth < maxDepth - % Рекурсивный вызов для вложенных структур с увеличением глубины + % Рекурсивный вызов для вложенных структур periphConfig.clear_tab_params(mask, value, paramName, depth + 1); end else + % Очищаем параметры Sources/Includes if strcmp(key, 'Sources') || strcmp(key, 'Includes') baseName = configJs.get_final_name_from_prefix(prefix); periphConfig.clear_single_periph_code_param(mask, baseName); @@ -263,12 +286,14 @@ classdef periphConfig end end - function sync_tab_params(mask, configStruct, prefix, depth) + % Рекурсивная синхронизация параметров вкладки + % Используется при включении вкладки + if nargin < 4 depth = 0; end - maxDepth = 3; % Максимальная глубина рекурсии + maxDepth = 3; fields = fieldnames(configStruct); @@ -279,7 +304,6 @@ classdef periphConfig if isstruct(value) if depth < maxDepth - % Рекурсивный вызов для вложенных структур с увеличением глубины periphConfig.sync_tab_params(mask, value, paramName, depth + 1); end else @@ -292,13 +316,15 @@ classdef periphConfig end function create_all_code_storage_params(blockPath, config) + % Создание всех скрытых параметров для хранения кода периферии + mask = Simulink.Mask.get(blockPath); containerName = 'configTabAll'; container = mask.getDialogControl(containerName); existingParams = mcuMask.collect_all_parameters(container); - % Убедимся, что контейнер существует + % Создаем скрытую вкладку для хранения параметров кода tabName = 'hiddenCodeTab'; tab = mask.getDialogControl(tabName); if isempty(tab) @@ -309,20 +335,22 @@ classdef periphConfig tab.Visible = 'off'; end - % Запуск рекурсивного обхода + % Рекурсивный обход конфигурации для создания параметров periphConfig.process_struct_recursive(mask, tabName, config, {}, existingParams); end function process_struct_recursive(mask, tabName, currentStruct, nameStack, existingParams) + % Рекурсивный обход структуры конфигурации + % Создает скрытые параметры для всех Sources и Includes + fields = fieldnames(currentStruct); for i = 1:numel(fields) fieldName = fields{i}; value = currentStruct.(fieldName); - newStack = [nameStack, fieldName]; % Добавляем уровень к имени + newStack = [nameStack, fieldName]; - % Если value — структура, обходим дальше if isstruct(value) - % Проверяем: это структура с Sources/Includes или просто промежуточный узел? + % Проверяем наличие Sources и Includes в структуре hasSources = isfield(value, 'Sources'); hasIncludes = isfield(value, 'Includes'); @@ -339,33 +367,37 @@ classdef periphConfig end end - function cleanup_obsolete_code_params(blockPath, config) + % Очистка устаревших скрытых параметров + % Удаляет параметры для периферии, которой больше нет в конфигурации + mask = Simulink.Mask.get(blockPath); maskParams = mask.Parameters; - % Получаем список актуальных периферий + % Получаем список актуальных периферийных модулей validPeriphs = fieldnames(config); for i = 1:numel(maskParams) paramName = maskParams(i).Name; - % Проверяем, является ли параметром хранения Sources или Includes + % Ищем параметры хранения кода expr = '^Hidden_(\w+)_(Sources|Includes)$'; tokens = regexp(paramName, expr, 'tokens'); if ~isempty(tokens) periph = tokens{1}{1}; - % Если периферии больше нет – удаляем параметр + % Удаляем параметр если периферия больше не существует if ~ismember(periph, validPeriphs) mask.removeParameter(paramName); - fprintf('Удалён устаревший параметр: %s\n', paramName); end end end end function codeStruct = restore_periph_code_from_mask(blockPath) + % Восстановление кода периферии из скрытых параметров маски + % Собирает все Sources и Includes в одну структуру + mask = Simulink.Mask.get(blockPath); maskParams = mask.Parameters; @@ -375,26 +407,27 @@ classdef periphConfig for i = 1:numel(maskParams) name = maskParams(i).Name; - % Ищем параметры Sources + % Поиск параметров Sources tokensSrc = regexp(name, '^Hidden_(\w+)_Sources$', 'tokens'); if ~isempty(tokensSrc) val = maskParams(i).Value; if ischar(val) || isstring(val) valStr = strtrim(char(val)); - % Пропускаем пустые строки и '[]' + % Пропускаем пустые значения if isempty(valStr) || strcmp(valStr, '[]') continue; end + % Разбиваем на строки и фильтруем пустые lines = splitlines(valStr); lines = lines(~cellfun(@(x) all(isspace(x)) || isempty(x), lines)); - allSources = [allSources; lines]; %#ok + allSources = [allSources; lines]; end continue; end - % Ищем параметры Includes + % Поиск параметров Includes tokensInc = regexp(name, '^Hidden_(\w+)_Includes$', 'tokens'); if ~isempty(tokensInc) val = maskParams(i).Value; @@ -407,27 +440,28 @@ classdef periphConfig lines = splitlines(valStr); lines = lines(~cellfun(@(x) all(isspace(x)) || isempty(x), lines)); - allIncludes = [allIncludes; lines]; %#ok + allIncludes = [allIncludes; lines]; end continue; end end + % Формируем результирующую структуру codeStruct = struct(); codeStruct.Sources = allSources; codeStruct.Includes = allIncludes; 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); % Удаляем все параметры @@ -439,30 +473,27 @@ classdef periphConfig end end - % Рекурсивно удалить все вкладки внутри контейнера + % Рекурсивно удаляем все вкладки mcuMask.delete_all_tabs(mask, container); end - function res = addCodeBat(codeConfig, codePath) - % Добавить сурсы и пути в батник - % Возвращает 0 при успехе, 1 при ошибке + % Добавление кода периферии в BAT-файл компиляции + try - % Формируем строки + % Формируем строки для BAT-файла srcText = compiler.createSourcesBat('code_PERIPH', codeConfig.Sources, codePath); incText = compiler.createIncludesBat('includes_PERIPH', codeConfig.Includes, codePath); - % Записываем результат - res = compiler.updateRunMexBat(srcText, incText, ':: PERIPH BAT'); % Всё прошло успешно + % Обновляем BAT-файл + res = compiler.updateRunMexBat(srcText, incText, ':: PERIPH BAT'); catch - % В случае ошибки просто возвращаем 1 - res = 1; + res = 1; % Ошибка end end function res = addUserFunctions(userCodeConfig) - % Добавить функции и дефайны в исходный код wrapper - % userCodeConfig — структура config.UserCode + % Добавление пользовательских функций в код обёртки initFuncsText = ''; simFuncsText = ''; @@ -471,44 +502,46 @@ classdef periphConfig if isfield(userCodeConfig, 'Functions') funcs = userCodeConfig.Functions; + % Обрабатываем функции инициализации if isfield(funcs, 'PeriphInit') initFuncs = funcs.PeriphInit; initFuncsText = strjoin(strcat('\t', initFuncs, ';'), '\n'); end + % Обрабатываем функции симуляции if isfield(funcs, 'PeriphSimulation') simFuncs = funcs.PeriphSimulation; simFuncsText = strjoin(strcat('\t', simFuncs, ';'), '\n'); end + % Обрабатываем функции деинициализации if isfield(funcs, 'PeriphDeinit') deinitFuncs = funcs.PeriphDeinit; deinitFuncsText = strjoin(strcat('\t', deinitFuncs, ';'), '\n'); end + % Записываем функции в файл обёртки res = periphConfig.writeWrapperCode(initFuncsText, simFuncsText, deinitFuncsText); end end function res = writeWrapperCode(initFuncsText, simFuncsText, deinitFuncsText) - % Входные параметры: - % srcText - текст для записи set code_... - % incText - текст для записи set includes_... - % - % Возвращает: - % res - 0 при успехе, 1 при ошибке + % Запись пользовательских функций в файл mcu_wrapper.c + wrapPath = fullfile(mcuPath.get('wrapperPath'), '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'); + % Записываем обновленный файл + fid = fopen(wrapPath, 'w', 'n'); if fid == -1 error('Не удалось открыть файл для записи'); end @@ -521,18 +554,22 @@ classdef periphConfig end function addConfig(mask, containerName, periphName, defPrompt, def, rowCountMap, rowWidth) - % mask — объект маски Simulink.Mask.get(blockPath) - % containerName — имя контейнера, в который добавляем параметр (например, 'configTabAll') - % periphName — имя вкладки / контейнера для текущего периферийного блока (например, 'ADC') - % defPrompt — имя параметра в Defines (например, 'shift_enable') - % def — структура с описанием параметра (Prompt, Def, Type, Default, NewRow и т.п.) - + % Добавление одного параметра конфигурации в маску + % mask - объект маски + % containerName - имя контейнера + % periphName - имя периферийного модуля + % defPrompt - имя параметра + % def - структура с описанием параметра + % rowCountMap - карта для отслеживания строк + % rowWidth - ширина строки + + % Инициализация счетчика строк для этого модуля if ~isKey(rowCountMap, periphName) rowCountMap(periphName) = 0; end rowCount = rowCountMap(periphName) + 1; - % Устанавливаем NewRow, если он не задан + % Определяем начинать ли новую строку if ~isfield(def, 'NewRow') def.NewRow = mod(rowCount - 1, rowWidth) == 0; elseif def.NewRow == true @@ -540,20 +577,20 @@ classdef periphConfig end rowCountMap(periphName) = rowCount; - % Найдем контейнер с таким именем + % Получаем контейнер 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'; @@ -562,20 +599,20 @@ classdef periphConfig case 'popup' paramType = 'popup'; otherwise - % Игнорируем остальные типы - return; + return; % Пропускаем неизвестные типы end + % Создаем валидное имя параметра paramName = matlab.lang.makeValidName(defPrompt); - % Получаем значение + % Устанавливаем значение по умолчанию if strcmp(paramType, 'popup') if isfield(def, 'Def') && iscell(def.Def) && ~isempty(def.Def) choices = def.Def; - valStr = ''; % по умолчанию — ничего + valStr = ''; elseif isfield(def, 'Options') choices = def.Def; - valStr = ''; % по умолчанию — ничего + valStr = ''; else warning('Popout параметр "%s" не содержит допустимого списка в Def.', defPrompt); return; @@ -632,16 +669,15 @@ classdef periphConfig end end - callback = sprintf('periphConfig.periphParamCallback("%s");', paramName); + callback = sprintf('try periphConfig.periphParamCallback("%s"); catch end', paramName); param.Callback = callback; end - - - %% ELEMENTARY + %% ELEMENTARY FUNCTIONS - базовые вспомогательные функции function clear_single_periph_code_param(mask, periph) - % Очистка кода одного поля конфига + % Очистка кода одного модуля периферии + paramNames = { ['Hidden_' char(periph) '_Sources'], ['Hidden_' char(periph) '_Includes'] @@ -653,14 +689,14 @@ classdef periphConfig param = mask.getParameter(paramName); param.Value = ''; catch - % Параметр не существует — ничего не делаем + % Параметр не существует - игнорируем end end end function store_single_periph_code(mask, periph, code) - % Запись кода одного поля конфига - % Сохраняем Sources, если они есть + % Сохранение кода одного модуля периферии в скрытые параметры + if isfield(code, 'Sources') paramName = ['Hidden_' char(periph) '_Sources']; try @@ -671,7 +707,6 @@ classdef periphConfig end end - % Сохраняем Includes, если они есть if isfield(code, 'Includes') paramName = ['Hidden_' char(periph) '_Includes']; try @@ -683,15 +718,13 @@ classdef periphConfig end end - - function res = ternary(cond, valTrue, valFalse) + % Вспомогательная функция - тернарный оператор if cond res = valTrue; else res = valFalse; end end - end -end +end \ No newline at end of file diff --git a/mcuwrapper.prj b/mcuwrapper.prj index f556f14..713f663 100644 --- a/mcuwrapper.prj +++ b/mcuwrapper.prj @@ -1,5 +1,5 @@ - + MCU Wrapper Razvalyaev wot890089@mail.ru @@ -7,7 +7,7 @@ Library for run MCU program in Simulink - 1.02 + 1.04 ${PROJECT_ROOT}\MCU Wrapper.mltbx @@ -100,14 +100,12 @@ - C:\Program Files\MATLAB\R2023a + C:\Program Files\MATLAB\R2021b - - @@ -129,16 +127,6 @@ true - - - true - - - - - true - - false