From 49be34efc9e20797526d8256e5c46864d9d6c425 Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Fri, 7 Nov 2025 14:52:52 +0300 Subject: [PATCH] pre-release 1.04 --- MCU Wrapper.mltbx | Bin 86036 -> 95043 bytes McuLib/lib/McuLib.slx | Bin 24124 -> 24145 bytes McuLib/m/appWrap.m | 127 ++++++++++++----- McuLib/m/asynchManage.m | 67 +++++---- McuLib/m/compiler.m | 94 ++++++++----- McuLib/m/configJs.m | 80 +++++++---- McuLib/m/customtable.m | 121 +++++++++------- McuLib/m/editCode.m | 113 ++++++++------- McuLib/m/installTemplates.m | 2 +- McuLib/m/mainConfig.m | 96 +++++++------ McuLib/m/mcuMask.m | 152 ++++++++++---------- McuLib/m/mcuPath.m | 123 +++++++++++------ McuLib/m/mcuPorts.m | 139 ++++++++++++------- McuLib/m/mexing.m | 266 ++++++++++++++++++----------------- McuLib/m/periphConfig.m | 267 ++++++++++++++++++++---------------- mcuwrapper.prj | 3 +- 16 files changed, 974 insertions(+), 676 deletions(-) diff --git a/MCU Wrapper.mltbx b/MCU Wrapper.mltbx index b92d2d8b641be2332d20735e3ed19f883b94cbfc..2b8619c7e5326f53dd64b069f6ecf9b739f66342 100644 GIT binary patch delta 74718 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%0DWaj!!wfHFax#MDESQLxHpFE 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 *&Efr^&kg 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­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 diff --git a/McuLib/lib/McuLib.slx b/McuLib/lib/McuLib.slx index 1432d09455704be95f0b483fb39e02347aa4a757..b0f19a48691dbab1b2570cf91270f511664d9a90 100644 GIT binary patch delta 6928 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 BGX?+v delta 6900 zcmY*;Wl$7=)BaIPAKij<9UR?~(s8uH0fKZRC4F@F(GAiKhjgbR93_HugLJp>`uoSs z``ekF*_e4g?1z1x*`4Y{>Fh+Q-yy+Po@K5oCIkS6gW;zn@~|XnZVT;V&`6vmDWCk% zTlWyQ5gTr1{VmM}o3@VMG0ehK{W8HM{+Pi{GWqwFOv#Lk4Hr0D;7e5> zst8+1vYoo#fRWJSWLp^9%}}@L)zBMEa<<$ew>M_(hbUd|7I1?SHV*yHy2AQP>#o8z z3QI~~)b-TZ9+6g<*(x|S2uM+PcFB8;*Pr@2O{FY0GgLjKXE!nbj!qSJ=bZ1>H|}OW zIbKcz*)=hnOSO3E3A9z%#QFEHv?RDY_<9fNQ^%s0!EOxigxbuN+BM>6f(o6sYIo|v zr~rY{tQtd>q$DlTqTgfzWN*=|W7Wq~UNsxFiHXapAWA6tK4e8WyUn3PJ8|cEvH(sE z@BL~hx}l3R_##(^Rixcqm}}~Qzb#Qd~%enKGe2c4`CHVA%w4M$5Z=qp;mcEvO=o_V{I!fc5~$${p!4LWXr52~LKQ4}t#U znJwNaUIFQbGaYfcs&i$fTVzS<3%_c<{0Vu})*$>hen2`XU_}@IuH}Z?IN09tubA(_ zT+OoXl9P^_ykVA%MQ_P%yIo5K-?I4$j9D=Z^HZHn$I9OXi#5s>&qWk8n3?vy$>}Ef z?HRhj4!`Mi+4979zVf2xm@#1i40QD+DWNw}LO^VXgu9 z;mxs(tIjf>Chnv&=%tMrjNQ%2v-3D{1cG*^TLEYzK%}VaYf6sW9@~|c3&^BL;w6>~ zx)mzVnp*}+!6u~rLn{#wiE$pSTzZ!xyDiekj&VbW+A^6UP2Jav`rOIH;*_#B6jM1~ z(Ym^FVv>Y7ZB$V8kQVu@}u3SR_F^@LaYi z9^#t8nNk(e;5JaKpNmD8TjXL&u#=G`E9{`!!o^!q$El|lz5IN#G`!Llwu5Ca*6AEW zBOKs>Oo$S4z0Ja$^TcOJxOM>Nj~S2bSf>1}cToPrb-aKFt>*d_Z#uNSlo{VhR%$7* zaNpz{NlEmc=RTEVdc1QJR)6tf?PVTAOJEr=*MrzKy5>B<6!-0`kRe+0>$05X-ZwTb zzq~K2KEKLL_LHPzmwx^cbYFu&qQ0NVpwOVo?^LE3c{lcGS|Hko)QvVG z_elWL zY%?PcVg^;M!pZ&1Ft#e%*d@PadONxUk9zEWmpZ-kczs`r+0V3Bv#N7>F=@9ut6g%4 z@hmP9)J30x%MG3R&#^5nn6)aB3&z}G@S zQP{cU*vKoBffiH1=#Bo}yCC_#2nE>IygZ#})ZX6lup>wtmL4Ow=H4sZF3fBXX8s{k z{@BqQ(~eM=ME4uDn&w+>_>A#5OzU_Uf){5RON`a9)9~2485VSg5(eUY@ZSnyMYkL; zC?3-wE}e7o_hKi@om1&yM?S6F0a#nRioYdCna}>9hNk*>dwhA^kq!Fr3gBixQJ{_R z`>9z@Nx<9!)BiNugL^2%rOt}?D?LyeOZ%IK;d}~Xsr=P@RBPZn{f%;HZKuKF%6xS? z?1M?o&wWm6s0Wjmt(zOBN*mIF->0v-cru#{YRUAKnGzQ#Nspl~i+VRr9?ROOQ*8Z{ zd}4cJF`KxyIZO+^xIYoYMUHa^v1T(vhV69ox~JI*R>$bJZ?8Igbw7G^Um zDDQI{!~DuJ@^k4g?tb5pVLy&a)0A&&nRLB>H`4*pF!t~-cE?v33R*gBYr+f?BZ{v7 zK_2TziXC+R%^?^Dx;r@m|GZ12#aHo?N(@-;gpfs3^_NEEged&Ym9y z3^q~;j}IQ!mL#TosoWl5y%y)P_fCq!hp{sP_4qFiiMx8s)zdEdM(b;#*Hx8xLr&WK z15Y`0g_nht4Bdr{LRKdnM!Rzo86xBi3!ufLYc1^%W@A<_j-PevaYxV_>X#e8-@4b6 z0%5)nLYs@3>-sohn9aDQw3cH(KK~pjib%82WVd+R?5G!=<-HN8jEH-|ETXGv$@j{J zGR_fw(}{DjwB#$7Cwp-)oqvwZ-QYI^;qZ4B7<%@R{%cxVObSR9LaJgjwnlY3&`g?W z_2^RgC+RL|ql(-?H|RF-#gBUvDSoID%>Zl?|M=2iLj<$Ys?{NkyJ7efH%Q^+zEMPc z33cYLF^A5ZQUIUXZyu+W2 zO07bSy;S)=YKJZzmM6x=Q2A3I!YOj}Mj6m2zI8Q-f533H=^9wc05x&Nx?i8B!gfX3;sE*I=Lrn{Sc5A?fu#1N0471lUx!O zwI1t5tmWlLstxPNXhe^-2&VL`pix)@ zFvfCFPY{FHsRj9JiUzHAn8@{=@(@ zZ!LF@iItkr;H#=^*Mr$RzXOUV2V#pYvcemLUf(M$ZL^Ur2Y*Jp_==I9qjpvf5kmyHR9kNzLV@jH$?bpL!+vWyGkDVLIX}l< zU)HIPTVWbZU|T_E#@+*yo1$@hzX=RI++~hwl*h)5Ww(yuTPKewbBMzHcqBSy-5W;?}V)HLc9JP%D^-zY6l?%o;Ny82`KWgge98a?}2uxcjQf=uCwRd+n>3e(3wVSLi%`b%Wp6I5xI1j06tg^abO9FD0lYC4qSXYXf5!(!`XhY;#CW?Kz1jMZx z>B;jP1FBGW&DMKmA;Gzhf%7ulLF?>P^i%MLF7KLx_3#Xw2j*-W{z#vv#V+`xfJbw! z08IJ=c~-yVW*VV9vf_TVrz5VGL2hjg>Js6gdxK&c9nvvBZVWywL>{4*Fow`9-$hXT z{$or58}p`JD=GR}eO_b!7?GQ6;5Sgk+%o4hBoy%>qN59Qdw;BaHi_#gt z{YWcwdGYP6&%KE`jhosakSM}W)>4_~2{UTI2)bZm-2Pn*2$7!O2bVCRWDRf9LK}!s z6S>f=l}^sB=K_D7rpq@& zV)>Mig%va^rVToau}oi;=|x8iuLxJ_EKRERBzS2jCNF#+A8wV*zOix9OHea0#i%^O z_QTBQ`ZrEOKGvN$!}~ps-E%g2)E<&7_1AWPK4Mf-nUje|FJGrOhacCsV?BuliSf`tPd?a)K9?&xF9`+jyG zL53%5G%u2_ckEKFrSJH4kIRbBmjt>}3jXkh4gc*${{?b7awDpOV`oo9uzxvQ!Slq9 zATL@LmMUS)u3pKa(pJk6Uqg=CHzXMW3zQ;1stI)?4 z0OB9mm8sIat*dX6UXmmHN#oQ96?!Kl$ace_W2oL^1f2koqQM2}=E_bNsRsMFUVF9I zjV+-hc7!j(+tT*=*9v(f!h?k=u-o2nT-?!6>vAfHOF`|e{kczTw+vT0WGZsln}2ow z58V_zs=(BJ;q*-jLv)_&C4!|9^;!`fk3t0>bngTB-R02f_JW3R4)q(SE+0+vTb)3> z#&z(M3f^kbWMLu+=+K)zy|-Y9gBgCOaiH zCf?|&Tm23k0jHyB{o$vs<#`q&TED}#50Htju8Zx4Zy{(DoL>!J6AiHenB7@soq|k+ zt6ZW{4_ZgPYDqzwcw`u=LRm4%*p6%8_lRQQrf4U{AkuBrqg+n*Vg19xHKFs+^Pw6c z7{=!(8_(9QjhGVv>nDjm43WG~BcIGE(n1*bLHxY$(^grW@%V!1BT!Qi8E?^_8^hc^ zu--Xe4)HGpu^;NS*5GbUjRsX#a#T*IUEE;>e30sHstMN?L=Xkevtcd}etIeHXqWZ_ zGNAruWcMSQ2VptS+BWDEIHiN;zs=Rv0b8vmP~F&KiDjjybp94vYMwPn=jQjR=R)Gu zSF=>l83Q>ym2}YVubRJ7&de4qg1$CztIp>`0Psx;RB8)6CE&!mZiiXYw001!mIX!@hn<@hNFG7I0FVWGd zg|wIvO#IR)dVCO2{h8vcd2?m5clkbu-jL$hK%;DKM+hYZ;99T|ENJbnLgnMK({r}E;n`3+ z$;Zf*L#l9ZJDv#&1&WvN>T1guzeTDUw5RJ@iN;^q6*=MkGw0Cs7{!{0RJF%*)h zk+X53+jLd5o$R_w^p5=d&}kxDY7OZ!zdQ(oNp6F{Yl6!;-;LHiBQ*c|;Y|6EIIHqG z2TomyWxS9p>=m6Q#Ajv=P9@c<)-6_|D&s1zU0_mnlalWco4g*m9O>bsFVV^&FRpH4 z`Un;)DmEPipjCycO}!~yP2JOHbd;H)IAZ7wExmH^g8@d!r6NS;UMRBYjsyDAjV7Q6;7tDvNEpjTq7Rp{G`V=XQ47f`Y!QI z$(lhNXI4S$HY?U6d3=&T>dUPF{YIHirA3+hMq@%=uNVT2y1_pKX3%eP6+83|=kUv8 zEcy6uHX0ugkxT2y@8{jpyj;(4K3o1iYkQ#3d^YKMRPr}BgMI6> zQ}VsGnnM8lf-~pHMO2*)YieR6XVUfUZ`qun@g}(mDMEk2e;XGW;+}VyKtn5F44Hgj zjL1}Hz&Ejo;8waaYztCMx&~-*3%Tm&Q4BKQyjLNo z6c=A2eOHwdkuZ%zTD>~m3x&H?_D`6mr^J9n1Nds!(R8lLHCTSgHG zpOXq6M6r$(3@YYZV=@wVe4jTK1!-X$EMCPU%Hb*up}Q$GuMKpyz0*Urd-}WB@|FBa z(t35^dkneOm;)NYAh32*XRl7#&m(o6$Kb#^d$kRz|MidW5QXS@mc2&3+rusc_~HZS zF?@XFS!PiuUFaFO?XUAO?#2R#@ma8eAnWk1(ywOVI4-?h7y{4+btWYE`G+$Wy!ngbbyhK2e?F7Uf@< zn};gFEYP2*KA1m6W4wxnRfkSy2jge-yuApbYAVZ^!2ZiF1=V~rcqDy;wduqQS(aVN zFebk!drE1c-^i(du%X+8GD0pprE_k`YKhk$eY48md2%6$n7!bA{cZ_o!Xf6g#~{nt z1I--5TKo6kexR~|p6Som*X~))Wslgp)qF=`yL1^n4IPj>k^;_|U0&=LMD`FYpE`SY zX#>eGo4P9^J{_9Tc2vD>-Z5~ z_r>?Xm%IG>Fv^3e{s)1c^;%llSm7E~k?I65UHh^UO!H1@U?Q<0BEV=9|8=4^m0^C(dy*_dInu(=a zzb^nh#}MznC#H^%2;mDi|!Dv;xhXRTQ5E9&zAb z40QkZ0|>>?1O*8i?D%{|N1${ZkqL%mNOBDxpzu z1K^rqDtIQ81bz#pLK6i4CufCA7;&J9^8DYP6AutBXh;Bm$BPfIH^N0xg?AZoy!sC( z8vTQM`X^W@{}1B-M<(G1z`^`fa9U#yln%JE@mmyOc)T&!|0B3wV^Wky_%CBH_5WJ= U0E+)Y;Dx_7;d~Kq@So!U0AcGYC;$Ke diff --git a/McuLib/m/appWrap.m b/McuLib/m/appWrap.m index cfb7a94..7b0f3da 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 + + % Сохраняем изменения прямо из MATLAB без внешних редакторов fid = fopen(filename, 'w', 'n', 'UTF-8'); 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..c33a4d6 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); + % Записываем обновленный BAT-файл fid = fopen(batPath, 'w', 'n', 'UTF-8'); 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..e50b495 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,17 +86,25 @@ 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); + + % Записываем JSON в файл fid = fopen(config_path, 'w', 'n', 'UTF-8'); if fid == -1 error('Не удалось открыть файл periph_config.json для записи.'); @@ -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 3a1aee0..f429b4f 100644 --- a/McuLib/m/mainConfig.m +++ b/McuLib/m/mainConfig.m @@ -1,10 +1,17 @@ classdef mainConfig + % Класс для экспорта и импорта конфигурации маски Simulink в JSON + % Позволяет сохранять и загружать настройки блока между сессиями methods(Static) function config = export() + % Экспортирует текущую конфигурацию маски в JSON файл + % Сохраняет параметры обёртки, портов и приложения + blockPath = gcb; mask = Simulink.Mask.get(blockPath); + + % Списки параметров для экспорта по категориям wrapParamToExport = {'wrapperPath', 'enableDebug', 'mcuClk', ... 'threadCycles', 'enableThreading', 'enableDeinit', ... 'periphPath'}; @@ -22,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'); @@ -55,8 +65,6 @@ classdef mainConfig filepath = fullfile(path, file); - - % Сохраняем в файл fid = fopen(filepath, 'w'); if fid == -1 @@ -67,8 +75,10 @@ classdef mainConfig fclose(fid); end - function import() + % Импортирует конфигурацию маски из JSON файла + % Восстанавливает параметры обёртки, портов и приложения + % Получаем путь к текущему блоку blockPath = gcb; mask = Simulink.Mask.get(blockPath); @@ -105,19 +115,20 @@ classdef mainConfig 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); @@ -127,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); @@ -185,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 @@ -195,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) @@ -205,7 +214,7 @@ classdef mainConfig end case 'customtable' - % Массив строк + % Загружаем данные в таблицу if iscell(def.Default) customtable.collect(paramName, def.Default); else @@ -213,7 +222,7 @@ classdef mainConfig end case 'text' - % Просто текстовая строка + % Устанавливаем текстовое значение if ischar(def.Default) || isstring(def.Default) param.Value = char(def.Default); else @@ -221,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 @@ -229,7 +238,7 @@ classdef mainConfig end otherwise - % По умолчанию просто устанавливаем строковое значение + % Универсальная установка значения if ischar(def.Default) || isstring(def.Default) param.Value = char(def.Default); elseif isnumeric(def.Default) @@ -239,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 @@ -263,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 104fd17..bf209e3 100644 --- a/McuLib/m/mcuMask.m +++ b/McuLib/m/mcuMask.m @@ -1,156 +1,172 @@ 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'); if fid == -1 error('Не удалось открыть файл для записи.'); @@ -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,20 +310,18 @@ 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')); @@ -325,8 +329,8 @@ classdef mcuMask end function v = getMyLibVersion() - v = 'pre-1.03'; + % Получение версии библиотеки + 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..47cd144 100644 --- a/McuLib/m/mcuPorts.m +++ b/McuLib/m/mcuPorts.m @@ -1,36 +1,51 @@ 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'); if fid == -1 error('Не удалось открыть файл для записи'); @@ -38,7 +53,10 @@ classdef mcuPorts fwrite(fid, code); fclose(fid); + % Вставляем сгенерированную конфигурацию в файл реализации code = editCode.insertSection(cCode, '// INPUT/OUTPUTS AUTO-PARAMS', cText); + + % Записываем обновленный файл реализации fid = fopen(cPath, 'w', 'n', 'UTF-8'); if fid == -1 error('Не удалось открыть файл для записи'); @@ -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 1e3b9e2..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,23 +254,24 @@ 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 + % Выпадающие списки if strcmp(param.Alias, '') newDefine = ['-D"' def_name '"']; else @@ -272,9 +279,7 @@ function definesWrapperArg = addDefineByParam(definesWrapperArg, paramName, val_ end end - - - % Добавляем новый define к существующему (string) + % Добавление дефайна к результирующей строке if isempty(definesWrapperArg) || strlength(strtrim(definesWrapperArg)) == 0 definesWrapperArg = newDefine; else @@ -282,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() @@ -302,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', ... @@ -343,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}; @@ -354,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) @@ -387,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 a6a8b04..d04832c 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,43 +502,45 @@ 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'); if fid == -1 error('Не удалось открыть файл для записи'); @@ -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; @@ -636,12 +673,11 @@ classdef periphConfig 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 9b26494..d0ca6ac 100644 --- a/mcuwrapper.prj +++ b/mcuwrapper.prj @@ -7,7 +7,7 @@ Library for run MCU program in Simulink - 1.03 + 1.04 ${PROJECT_ROOT}\MCU Wrapper.mltbx @@ -84,7 +84,6 @@ ${PROJECT_ROOT}\McuLib - ${PROJECT_ROOT}\McuLib\.library_installed.mat ${PROJECT_ROOT}\McuLib\install_my_library.m ${PROJECT_ROOT}\McuLib\lib ${PROJECT_ROOT}\McuLib\m