LuaでのProteusの電子コンポーネントモデルの作成

いくつかの長期建設プロジェクトがありますが、その1つはCDP1802に基づくコンピューターの作成です。 メインボードは、紙とプロテウスでモデル化されました。

すぐに疑問が生じました。プロテウスに存在しない要素をどうするか?

Visual Studioで独自のC ++モデルを作成する方法については、多くのリソースで詳しく説明されています。

残念ながら、Linuxでビルドする場合、このオプションはあまり便利ではありません。 また、C ++を知らない場合、またはデバッグのためにその場でモデルを編集する必要がある場合はどうなりますか?

そして、私はモデリングに集中し、他のすべてを可能な限り単純化したいだけです。

そこで、Luaでスクリプトを使用してシミュレーションモデルを作成するというアイデアが浮上しました。

興味がある人のために、カットをお願いします(2Mb gif)。







なぜそれが必要なのですか



プロセッサモデルの作成など、あらゆる種類のエキゾチックなことを忘れてしまった場合、シミュレータで何かをする習慣を長い間失いました-センサーをさまざまな種類のデバッグ、手元のオシロスコープ、マルチメーター、JTAG / UARTに接続し、自分でデバッグします。

しかし、GPS /モーション障害などの場合にプログラムのロジックをチェックする必要があるときは、別のマイクロコントローラーでGPSエミュレーションを作成する必要がありました。

KWP2000プロトコルを使用してマシンのテレメトリを実行する必要がある場合、ライブデバッグは不便で危険です。 はい、もしあれば-なんて不快なのでしょう。

道路上またはどこかで紳士用キット全体を持ち運ぶ場所でデバッグ/テストする機能は単純に不便です(主に趣味のプロジェクトについてです)ので、シミュレーターの余地があります。



Visual Studio C ++およびGCC



私はすべてのソフトウェアをGCCで作成し、MSVSでは作成が困難な開発済みのライブラリとコードを使用して、GCCでモデルを作成したいと考えました。 問題は、mingw32 DLLの下でアセンブルされたプロテウスがハングすることでした。 __thiscallおよび同志による操作を含むさまざまな方法が試されましたが、アセンブラーコールハックのオプションは適合しませんでした。

このような問題について豊富な経験を持つ月光の友人が、仮想テーブルを使用してCのC ++インターフェイスを書き換える方法を提案し、示しました。 Linuxで「生産を中断することなく」アセンブルする可能性に加えて、理論的には、Fortranでさえモデルを作成する能力が必要です。



C ++で模倣する



実際の仮想クラスの「エミュレーション」のアイデアは次のようになります。

元のC ++仮想クラスヘッダーは次のようになります

class IDSIMMODEL { public: virtual INT isdigital ( CHAR* pinname ) = 0; virtual VOID setup ( IINSTANCE* instance, IDSIMCKT* dsim ) = 0; virtual VOID runctrl ( RUNMODES mode ) = 0; virtual VOID actuate ( REALTIME time, ACTIVESTATE newstate ) = 0; virtual BOOL indicate ( REALTIME time, ACTIVEDATA* newstate ) = 0; virtual VOID simulate ( ABSTIME time, DSIMMODES mode ) = 0; virtual VOID callback ( ABSTIME time, EVENTID eventid ) = 0; };
      
      







そして、これがCバージョンです。 これは擬似クラスとその仮想テーブルです



 struct IDSIMMODEL { IDSIMMODEL_vtable* vtable; };
      
      







次に、クラス内にある関数へのポインターを使用して構造体を作成します(それらを作成し、個別に宣言します)

 struct IDSIMMODEL_vtable { int32_t __attribute__ ( ( fastcall ) ) ( *isdigital ) ( IDSIMMODEL* this, EDX, CHAR* pinname ); void __attribute__ ( ( fastcall ) ) ( *setup ) ( IDSIMMODEL* this, EDX, IINSTANCE* inst, IDSIMCKT* dsim ); void __attribute__ ( ( fastcall ) ) ( *runctrl ) ( IDSIMMODEL* this, EDX, RUNMODES mode ); void __attribute__ ( ( fastcall ) ) ( *actuate ) ( IDSIMMODEL* this, EDX, REALTIME atime, ACTIVESTATE newstate ); bool __attribute__ ( ( fastcall ) ) ( *indicate ) ( IDSIMMODEL* this, EDX, REALTIME atime, ACTIVEDATA* data ); void __attribute__ ( ( fastcall ) ) ( *simulate ) ( IDSIMMODEL* this, EDX, ABSTIME atime, DSIMMODES mode ); void __attribute__ ( ( fastcall ) ) ( *callback ) ( IDSIMMODEL* this, EDX, ABSTIME atime, EVENTID eventid ); };
      
      







必要な関数を記述し、使用する「クラス」のインスタンスを1つ作成します

 IDSIMMODEL_vtable VSM_DEVICE_vtable = { .isdigital = vsm_isdigital, .setup = vsm_setup, .runctrl = vsm_runctrl, .actuate = vsm_actuate, .indicate = vsm_indicate, .simulate = vsm_simulate, .callback = vsm_callback, }; IDSIMMODEL VSM_DEVICE = { .vtable = &VSM_DEVICE_vtable, };
      
      







など、必要なすべてのクラスで。 このような構造体の呼び出しはあまり便利ではないため、ラッパー関数が作成され、いくつかの自動化、欠落、頻繁に使用される関数が追加されました。 この記事を書いている間でも、私は多くの新しいものを追加し、反対側から仕事を見ました。



「できる限りシンプルにしますが、簡単ではありません。」



その結果、コードが成長し、何かを変更する必要があるという感覚がますます高まっています。モデルを作成するのに、同じエミュレーターをマイクロコントローラー用に作成するよりも時間がかかりませんでした。 モデルをデバッグするプロセスでは、実験するために常に何かを変更する必要がありました。 小さなことごとにモデルを再構築する必要があり、Cでテキストデータを操作することには多くの要望があります。 これに興味があるおなじみの人々も、Cを恐れていました(誰かがTurbo Pascalを使用し、誰かがQ Basicを使用しています)。



Luaのことを思い出しました。C、高速、コンパクト、視覚的、動的なタイピングと完全に統合されています。 その結果、LuaのすべてのC関数を同じ名前で複製し、完全に自給自足の方法で、まったく再アセンブリを必要としないモデルを作成しました。 dllを取得して、Luaでのみモデルを記述できます。 シミュレーションを停止し、テキストスクリプトを修正して、再び戦闘に参加するのに十分です。

Luaでのモデリング



主なテストはProteus 7で実施されましたが、ゼロから作成され、8番目のバージョンにインポートされたモデルの動作は良好でした。



いくつかの簡単なモデルを作成し、その例で何がどのようにできるかを見ていきます。

グラフィカルモデル自体の作成方法については説明しません。 ここここで完全に説明いるため、コードの記述に焦点を当てます。

検討する3つのデバイスを次に示します。 最初は点滅するLEDから始めたいと思っていましたが、それではあまりにもつまらないと判断しました。

A_COUNTERから始めましょう。







これは、内部クロックジェネレーターを備えた最も単純なバイナリカウンターで、その出力はすべて出力です。



各モデルには、モデルの動作と外部との相互作用を記述するDLLがあります。 この場合、すべてのdllモデルは同じですが、スクリプトは異なります。 したがって、モデルを作成します。



モデルの説明




 device_pins = { {is_digital=true, name = "A0", on_time=100000, off_time=100000}, {is_digital=true, name = "A1", on_time=100000, off_time=100000}, {is_digital=true, name = "A2", on_time=100000, off_time=100000}, {is_digital=true, name = "A3", on_time=100000, off_time=100000}, --       --     {is_digital=true, name = "A15", on_time=100000, off_time=100000}, }
      
      







device_pinsは、デバイスピンの説明を含む必須のグローバル変数です。 この段階では、ライブラリはデジタルデバイスのみをサポートしています。 プロセスでのアナログおよび混合タイプのサポート。

is_digital-出力は論理レベルでのみ機能しますが、trueのみが可能です

name-グラフィックモデルの出力の名前。 これは完全に一致する必要があります-Proteus内の出力のバインディングは名前によって決まります。

残りの2つのフィールドは、ピコ秒単位のピンスイッチング時間を表しています。



必要なユーザー定義関数




実際、スクリプトで何かを作成する厳密な必要性はありません。 何も書けません-ダミーモデルがありますが、最小限の機能のためにdevice_simulate関数を作成する必要があります 。 この関数は、ノード(導体)の状態が変化したとき、たとえば論理レベルが変化したときに呼び出されます。 device_init関数があります。 モデルをロードした直後に1回呼び出されます(存在する場合)。

出力状態をレベルの1つに設定するには、 set_pin_state関数があります。最初の引数は出力の名前を取り、2番目は目的の状態(SHI、SLO、FLTなど)です。



まず、最初にすべての出力が論理0になっていることを確認してください。

出力は、たとえばA0などのグローバル変数と、グローバル環境テーブル_Gを介した文字列定数「A0」としての名前の両方で参照できます。

 function device_init() for k, v in pairs(device_pins) do set_pin_state(_G[v.name], SLO) end end
      
      







次に、カウンター自体を実装する必要があります。 マスターオシレーターから始めましょう。 これを行うために、timeとevent numberの2つの引数を取るtimer_callback関数があります。

出力状態を設定した後、device_initに次の呼び出しを追加します。

 set_callback(NOW, PC_EVENT)
      
      







PC_EVENTは、イベントコードを含む数値変数です(グローバルに宣言する必要があります)

NOWは、現在の時刻から0ピコ秒後にイベントハンドラーを呼び出す必要があることを意味します(関数は引数として1秒の写真を取ります)

そして、これが関数ハンドラです

 function timer_callback(time, eventid) if eventid == PC_EVENT then for k, v in pairs(device_pins) do set_pin_bool(_G[v.name], get_bit(COUNTER, k) ) end COUNTER = COUNTER + 1 set_callback(time + 100 * MSEC, PC_EVENT) end end
      
      







イベントでは、 set_pin_bool関数が呼び出されます 。この関数は、引数として2つの状態(1/0)のいずれかを受け入れることで出力を制御します。



この関数は非周期的なイベントをスケジュールするため、出力を切り替えた後、set_callbackが再度呼び出されることに気付くかもしれません。 時間設定の違いは、将来set_callbackが呼び出されるためです。そのため、時間差を追加する必要があり、時間には現在のシステム時間が含まれているだけです。



だからそれが起こった
 device_pins = { {is_digital=true, name = "A0", on_time=100000, off_time=100000}, {is_digital=true, name = "A1", on_time=100000, off_time=100000}, {is_digital=true, name = "A2", on_time=100000, off_time=100000}, {is_digital=true, name = "A3", on_time=100000, off_time=100000}, {is_digital=true, name = "A4", on_time=100000, off_time=100000}, {is_digital=true, name = "A5", on_time=100000, off_time=100000}, {is_digital=true, name = "A6", on_time=100000, off_time=100000}, {is_digital=true, name = "A7", on_time=100000, off_time=100000}, {is_digital=true, name = "A8", on_time=100000, off_time=100000}, {is_digital=true, name = "A9", on_time=100000, off_time=100000}, {is_digital=true, name = "A10", on_time=100000, off_time=100000}, {is_digital=true, name = "A11", on_time=100000, off_time=100000}, {is_digital=true, name = "A12", on_time=100000, off_time=100000}, {is_digital=true, name = "A13", on_time=100000, off_time=100000}, {is_digital=true, name = "A14", on_time=100000, off_time=100000}, {is_digital=true, name = "A15", on_time=100000, off_time=100000}, } PC_EVENT = 0 COUNTER = 0 function device_init() for k, v in pairs(device_pins) do set_pin_state(_G[v.name], SLO) end set_callback(0, PC_EVENT) end function timer_callback(time, eventid) if eventid == PC_EVENT then for k, v in pairs(device_pins) do set_pin_bool(_G[v.name], get_bit(COUNTER, k) ) end COUNTER = COUNTER + 1 set_callback(time + 100 * MSEC, PC_EVENT) end end
      
      









他のすべて-宣言、モデルの初期化などは、ライブラリ側で行われます。 もちろん、Cでも同じことができ、関数の名前が同じであるため、Luaをプロトタイプに使用できます。

シミュレーションを開始し、モデルの動作を観察します







デバッグ機能





主な目標は、モデルの作成とそのデバッグを容易にすることでした。したがって、有用な情報を表示するためのいくつかのオプションを検討します。



テキストメッセージ




メッセージログに出力するための4つの機能、最後の2つは自動的にシミュレーションの停止につながります



 out_log("This is just a message") out_warning("This is warning") out_error("This is error") out_fatal("This is fatal error")
      
      











Luaの機能のおかげで、必要な情報を簡単、便利、迅速かつ明確に表示できます。



 out_log("We have "..#device_pins.." pins in our device")
      
      







では、2番目のモデルであるROMチップに進みましょう。

ポップアップ


ROMをモデル化し、動作中に実行させます。

ここでの結論の発表は変わりませんが、まず、マイクロサーキットのプロパティを追加する必要があります-ファイルからメモリダンプをロードする機能:







これは、モデルを作成するときにテキストスクリプトで行われます。

{FILE = "画像ファイル"、FILENAME、FALSE、画像/ *。BIN}




シミュレーションを一時停止したときに、メモリの内容、アドレスバスの内容、データバスの内容、動作時間など、モデルに関する重要な情報を確認できるようにしましょう。 便利な形式でバイナリデータを出力するには、memory_popupがあります。

 function device_init() local romfile = get_string_param("file") rom = read_file(romfile) mempop, memid = create_memory_popup("My ROM dump") set_memory_popup(mempop, rom, string.len(rom)) end function on_suspend() if nil == debugpop then debugpop, debugid = create_debug_popup("My ROM vars") print_to_debug_popup(debugpop, string.format("Address: %.4X\nData: %.4X\n", ADDRESS, string.byte(rom, ADDRESS))) dump_to_debug_popup(debugpop, rom, 32, 0x1000) elseif debugpop then print_to_debug_popup(debugpop, string.format("Address: %.4X\nData: %.4X\n", ADDRESS, string.byte(rom, ADDRESS))) dump_to_debug_popup(debugpop, rom, 32, 0x1000) end end
      
      





on_suspend関数 、一時停止中に呼び出されます(ユーザーが宣言した場合)。 ウィンドウが作成されていない場合は、作成します。

メモリはポインターとしてライブラリに転送されるため、後で何かを解放する必要はありません。すべてがLuaガベージコレクターによって実行されます。 そして、変数が必要なタイプのデバッグウィンドウを作成し、マスキングのためにオフセット0x1000から32バイトをダンプします。







最後に、OE、VPP、およびその他のCEの結論を無視して、ROM自体のアルゴリズムを実装します



 function device_simulate() for i = 0, 14 do if 1 == get_pin_bool(_G["A"..i]) then ADDRESS = set_bit(ADDRESS, i) else ADDRESS = clear_bit(ADDRESS, i) end end for i = 0, 7 do set_pin_bool(_G["D"..i], get_bit(string.byte(rom, ADDRESS), i)) end end
      
      











「デバッガ」に対して何かをしましょう。

データバスの内容を出力するソフトウェアUARTを作成します
 device_pins = { {is_digital=true, name = "D0", on_time=1000, off_time=1000}, {is_digital=true, name = "D1", on_time=1000, off_time=1000}, {is_digital=true, name = "D2", on_time=1000, off_time=1000}, {is_digital=true, name = "D3", on_time=1000, off_time=1000}, {is_digital=true, name = "D4", on_time=1000, off_time=1000}, {is_digital=true, name = "D5", on_time=1000, off_time=1000}, {is_digital=true, name = "D6", on_time=1000, off_time=1000}, {is_digital=true, name = "D7", on_time=1000, off_time=1000}, {is_digital=true, name = "TX", on_time=1000, off_time=1000}, } -- UART events UART_STOP = 0 UART_START = 1 UART_DATA=2 -- Constants BAUD=9600 BAUDCLK = SEC/BAUD BIT_COUNTER = 0 ----------------------------------------------------------------- DATA_BUS = 0 function device_init() end function device_simulate() for i = 0, 7 do if 1 == get_pin_bool(_G["D"..i]) then DATA_BUS = set_bit(DATA_BUS, i) else DATA_BUS = clear_bit(DATA_BUS, i) end end uart_send(string.format("[%d] Fetched opcode %.2X\r\n", systime(), DATA_BUS)) end function timer_callback(time, eventid) uart_callback(time, eventid) end function uart_send (string) uart_text = string char_count = 1 set_pin_state(TX, SHI) -- set TX to 1 in order to have edge transition set_callback(BAUDCLK, UART_START) --schedule start end function uart_callback (time, event) if event == UART_START then next_char = string.byte(uart_text, char_count) if next_char == nil then return end char_count = char_count +1 set_pin_state(TX, SLO) set_callback(time + BAUDCLK, UART_DATA) end if event == UART_STOP then set_pin_state(TX, SHI) set_callback(time + BAUDCLK, UART_START) end if event == UART_DATA then if get_bit(next_char, BIT_COUNTER) == 1 then set_pin_state(TX, SHI) else set_pin_state(TX, SLO) end if BIT_COUNTER == 7 then BIT_COUNTER = 0 set_callback(time + BAUDCLK, UART_STOP) return end BIT_COUNTER = BIT_COUNTER + 1 set_callback(time + BAUDCLK, UART_DATA) end end
      
      











性能



私を心配させた興味深い質問。 Proteus 7に付属の4040バイナリカウンターのモデルを使用して、アナログを作成しました。

パルス発生器を使用して、100 kHzの周波数の蛇行を両方のモデルの入力に供給しました



プロテウスの4040 = 15-16%CPU負荷

C上のライブラリ= 25-28%CPU負荷

ライブラリおよびLua 5.2 = 98-100%CPU負荷

ライブラリとLua 5.3a = 76-78%CPU負荷



ソースを比較しませんでしたが、明らかにバージョン5.3で仮想マシンを非常に最適化しました。 それにもかかわらず、仕事の利便性のために非常に寛容です。

そして、私は最適化の問題にも対処し始めませんでした。



このプロジェクト全体は自発的なアイデアとして生まれたものであり、さらに多くのことが必要です。

即時計画







もちろん、コードの作成に参加し、アイデアやフィードバックを手伝うことで、助けたいと考えている志を同じくする人々を見つけたいです。 実際、今では、私が必要とする目標と目的のために多くがハードコードされています。



広告とSMSなしでダウンロード



コード付きリポジトリ

GDBの完成したライブラリとデバッグシンボルはこちらです。



All Articles