あぐちゃんさんの物置き場

オタク、ブレボを溶かしがち

絶縁型DCDCコンバーターを作った話


経緯ってなんですか

三相インバーターの作成を目標としていろいろ試す中、やはりフルNchで組みたいというのがあるので、ハイサイドのゲートドライブの電圧を何とかする必要があります。ブートストラップで最初やるつもりでいたのですが、やはり何かと不安要素が多いので、フローティング電源として出力が三系統ある絶縁DCDCコンバーターを作ってしまえばいいや、という結論に至りました。

前に書いた絶縁DCDCの記事今見ると意味わかんないですね。なにしてんのこいつ。

回路

これ f:id:Agchan_Luice:20210905140006p:plain

ブレボ上であーだこーだやってうまくいったやつです。ブレボ上でやったときは負荷30Ωで最大2.8Wの出力が得られました。トランスのサイズの割にはしょぼいね。トランジスタのベース抵抗とかも適当に1kぶち込んだり電流減らしたいところは100kとスピコン突っ込んでみたりとだいぶ適当な構成となっております。真ん中下あたりでいっぱいピンあるのはトランスにつながってる部分です。

タイマICを買い忘れてたので無安定マルチとコンパレータで適当にデッドタイムの調整をしてます。ハイサイドへの信号伝達はその辺に落ちてたTLP250でやってしまいました。

駆動するMosFETがk4017なのでオーバースペックですがまともに使えるのがこれしか手持ちにありませんでした。速いフォトカプラなんかいいやつないんですかね。

はんだづけしたった

f:id:Agchan_Luice:20210905140650p:plain

手持ちのユニバ1枚に入らなかったのでフルブリッジ部分とトランス&整流で基板分けました。

FETのそばにある10kΩは回路図上で忘れてたプルダウンが居ますが他は変更ないはずですたぶん。 フルブリッジはちゃんと動作したので、あとは整流用のブリッジに使う1s4が秋月から届けば完成する予定です。

トランスなんもわからん

回路に使ったトランス(ほんまか?)は適当なコアを買ってきてエナメル線を巻いた自家製です。以前の記事で作ったやつは巻き数も少ないわ結合もゴミだわで使い物になりませんでした。あやつは巻きなおしてチョークコイル行きです。

もう巻きたくない。

まともなトランスボビンを買って巻くのが一番いいはずなんですが、どこから買えばいいのやら。

Minecraft.1.12.2 Forge IntelliJ IDEA mcmod.infoやassetsの中が読み込めない

注意

Mod制作初心者が書いた記事です。ここの情報を鵜呑みにして問題が起きても知りません。それでもいいよっていう同志初心者の皆々様は続きをお読みください。




具体的な問題の内容

IntelliJMinecraftのMODを作成するときに、Tasks/fg_runs/runClientから実行すると、resources中のファイル/フォルダにアクセスできない。故に、mcmod.infoやアイテム、ブロックのテクスチャを読み込むことができない。ファイルを配置したにもかかわらず、FileNotFoundExceptionエラーが出まくる。


環境

Windows 10
JDK 8u211
Minecraft 1.12.2
Minecraft Forge 1.12.2 (14.23.5.2838)
IntelliJ IDEA 2021.1.1




コード

build.gradle

buildscript {
    repositories {
        maven { url = 'https://maven.minecraftforge.net' }
            mavenCentral()
    }
    dependencies {
        classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '4.1.+', changing: true
        }
}

apply plugin: 'net.minecraftforge.gradle'

group = 'jp.agchan_san'
version = '1.0.0'
archivesBaseName = 'ninino_mod'


java.toolchain.languageVersion = JavaLanguageVersion.of(8) // Mojang ships Java 8 to end users, so your mod should target Java 8.

minecraft {
    // The mappings can be changed at any time, and must be in the following format.
    // Channel:   Version:
    // snapshot   YYYYMMDD   Snapshot are built nightly.
    // stable     #          Stables are built at the discretion of the MCP team.
    // official   MCVersion  Official field/method names from Mojang mapping files
    //
    // You must be aware of the Mojang license when using the 'official' mappings.
    // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
    //
    // Use non-default mappings at your own risk. they may not always work.
    // Simply re-run your setup task after changing the mappings to update your workspace.
    mappings channel: 'stable', version: '39-1.12'
    // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable.

    // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')

    // Default run configurations.
    // These can be tweaked, removed, or duplicated as needed.
    runs {
        client {
            workingDirectory project.file('run')

            // Recommended logging data for a userdev environment
            // The markers can be changed as needed.
            // "SCAN": For mods scan.
            // "REGISTRIES": For firing of registry events.
            // "REGISTRYDUMP": For getting the contents of all registries.
            property 'forge.logging.markers', 'REGISTRIES'

            // Recommended logging level for the console
            // You can set various levels here.
            // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
            property 'forge.logging.console.level', 'debug'

            mods {
                redenginemod {
                    source sourceSets.main
                }
            }
        }

        server {
            workingDirectory project.file('run')

            // Recommended logging data for a userdev environment
            // The markers can be changed as needed.
            // "SCAN": For mods scan.
            // "REGISTRIES": For firing of registry events.
            // "REGISTRYDUMP": For getting the contents of all registries.
            property 'forge.logging.markers', 'REGISTRIES'

            // Recommended logging level for the console
            // You can set various levels here.
            // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
            property 'forge.logging.console.level', 'debug'

            mods {
                redenginemod {
                    source sourceSets.main
                }
            }
        }
    }
}

// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }

dependencies {
    // Specify the version of Minecraft to use, If this is any group other then 'net.minecraft' it is assumed
    // that the dep is a ForgeGradle 'patcher' dependency. And it's patches will be applied.
    // The userdev artifact is a special name and will get all sorts of transformations applied to it.
    minecraft 'net.minecraftforge:forge:1.12.2-14.23.5.2855'

    // You may put jars on which you depend on in ./libs or you may define them like so..
    // compile "some.group:artifact:version:classifier"
    // compile "some.group:artifact:version"

    // Real examples
    // compile 'com.mod-buildcraft:buildcraft:6.0.8:dev'  // adds buildcraft to the dev env
    // compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env

    // The 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime.
    // provided 'com.mod-buildcraft:buildcraft:6.0.8:dev'

    // These dependencies get remapped to your current MCP mappings
    // deobf 'com.mod-buildcraft:buildcraft:6.0.8:dev'

    // For more info...
    // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
    // http://www.gradle.org/docs/current/userguide/dependency_management.html
}

// Example for how to get properties into the manifest for reading by the runtime..
jar {
    manifest {
        attributes([
                "Specification-Title": "NininoMod",
                            //"Specification-Vendor": "NininoMod authors",
                            "Specification-Version": "1", // We are version 1 of ourselves
                "Implementation-Title": project.name,
                "Implementation-Version": project.version,
                            //"Implementation-Vendor": "nininomod authors",
                                        "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
        ])
    }
}

jar.finalizedBy('reobfJar')

原因と解決策

解はここにありました。

qiita.com

どうやら、GradleとIntelliJがビルドする際の出力先がそれぞれ違うとのこと。なので、build.gradleの内容を少し書き換えます。プラグインを読み込んでいる部分と、archiveBaseNameの下に、出力先をそろえるためのコードを少し書き足します。


build.gradle

buildscript {
    repositories {
        maven { url = 'https://maven.minecraftforge.net' }
            mavenCentral()
    }
    dependencies {
        classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '4.1.+', changing: true
        }
}

apply plugin: 'net.minecraftforge.gradle'



//追加
apply plugin: 'idea'



group = 'jp.agchan_san'
version = '1.0.0'
archivesBaseName = 'ninino_mod'



//追加
//#####################################################
sourceSets {
    main {
            output.resourcesDir = java.outputDir
        }
}
idea.module.outputDir file("out/production/classes")
//#####################################################



java.toolchain.languageVersion = JavaLanguageVersion.of(8) // Mojang ships Java 8 to end users, so your mod should target Java 8.

minecraft {
    // The mappings can be changed at any time, and must be in the following format.
    // Channel:   Version:
    // snapshot   YYYYMMDD   Snapshot are built nightly.
    // stable     #          Stables are built at the discretion of the MCP team.
    // official   MCVersion  Official field/method names from Mojang mapping files
    //
    // You must be aware of the Mojang license when using the 'official' mappings.
    // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
    //
    // Use non-default mappings at your own risk. they may not always work.
    // Simply re-run your setup task after changing the mappings to update your workspace.
    mappings channel: 'stable', version: '39-1.12'
    // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable.

    // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')

    // Default run configurations.
    // These can be tweaked, removed, or duplicated as needed.
    runs {
        client {
            workingDirectory project.file('run')

            // Recommended logging data for a userdev environment
            // The markers can be changed as needed.
            // "SCAN": For mods scan.
            // "REGISTRIES": For firing of registry events.
            // "REGISTRYDUMP": For getting the contents of all registries.
            property 'forge.logging.markers', 'REGISTRIES'

            // Recommended logging level for the console
            // You can set various levels here.
            // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
            property 'forge.logging.console.level', 'debug'

            mods {
                redenginemod {
                    source sourceSets.main
                }
            }
        }

        server {
            workingDirectory project.file('run')

            // Recommended logging data for a userdev environment
            // The markers can be changed as needed.
            // "SCAN": For mods scan.
            // "REGISTRIES": For firing of registry events.
            // "REGISTRYDUMP": For getting the contents of all registries.
            property 'forge.logging.markers', 'REGISTRIES'

            // Recommended logging level for the console
            // You can set various levels here.
            // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
            property 'forge.logging.console.level', 'debug'

            mods {
                redenginemod {
                    source sourceSets.main
                }
            }
        }
    }
}

// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }

dependencies {
    // Specify the version of Minecraft to use, If this is any group other then 'net.minecraft' it is assumed
    // that the dep is a ForgeGradle 'patcher' dependency. And it's patches will be applied.
    // The userdev artifact is a special name and will get all sorts of transformations applied to it.
    minecraft 'net.minecraftforge:forge:1.12.2-14.23.5.2855'

    // You may put jars on which you depend on in ./libs or you may define them like so..
    // compile "some.group:artifact:version:classifier"
    // compile "some.group:artifact:version"

    // Real examples
    // compile 'com.mod-buildcraft:buildcraft:6.0.8:dev'  // adds buildcraft to the dev env
    // compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env

    // The 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime.
    // provided 'com.mod-buildcraft:buildcraft:6.0.8:dev'

    // These dependencies get remapped to your current MCP mappings
    // deobf 'com.mod-buildcraft:buildcraft:6.0.8:dev'

    // For more info...
    // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
    // http://www.gradle.org/docs/current/userguide/dependency_management.html
}

// Example for how to get properties into the manifest for reading by the runtime..
jar {
    manifest {
        attributes([
                "Specification-Title": "NininoMod",
                            //"Specification-Vendor": "NininoMod authors",
                            "Specification-Version": "1", // We are version 1 of ourselves
                "Implementation-Title": project.name,
                "Implementation-Version": project.version,
                            //"Implementation-Vendor": "nininomod authors",
                                        "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
        ])
    }
}

jar.finalizedBy('reobfJar')

自分の場合だとこれで、resourcesにアクセスできるようになりました。
くどいようですが、これで問題が起きても知りませんからね。

絶縁DCDCコンバーター とりあえずシミュレート編

制作に至った経緯

ハーフブリッジ回路を作ったのですが(これはまた後記事にします)、駆動するうえでフローティング電源があると非常に便利なんです。内部に電位が動きまくるところがあり、電流の供給が外部からあると動作が安定します(なくても動くけど制約が付きます)。最終的には3つのハーフブリッジを同時に駆動したいので、出力が3系統ある絶縁DCDCコンバーターを作ろうと思いました。欲しい電圧は5~8V程度で、電源は12Vを想定しているので、適当なフルブリッジと16:8:8:8のトランスで降圧します。

わざわざフルブリッジでやるのは、出力からのフィードバックができないため、トランスの巻き数比で出力電圧を調整できるからです。

f:id:Agchan_Luice:20210428113650j:plain
お手製トランス 結合?知らない子ですね.....

線材もっと細くすればよかった。


なんもわからん

意気込んだはいいもの、めちゃくちゃ難かしいななんだこれ。絶縁型なめてたわ。なんもわからん。
実際にトランスを巻いてフルブリッジを組んで適当な周波数の電流を流したのですが、案の定一瞬のうちに飽和して電源装置が電流制限をかけてました。飽和しないように周波数を上げて、なんとか1W程度の出力を得ました(あぶない)。インダクタンスはそれなりに確保できてるんでしょうけど、一体どんだけ結合小さいんだ。

f:id:Agchan_Luice:20210428114327j:plain
ブレボ上の様子
f:id:Agchan_Luice:20210428114354j:plain
1次側の電流の波形 だいたい±500mAくらい

Hブリッジの制御はゲートドライバ組むのめんどくさかったのでPIC16F1823でやっています。ゲートドライバICは高いからね。5V電源はめんどくさかったので5.1Vのツェナーで済ませました。上流は100Ω(定格オーバー)だし小食だからこれでたぶん足りるでしょう。で。



これ、危なくね?

今は電源が電源装置だから良いんですけど、バッテリーをつないだ時の挙動があまりにも不安です。なにせ、いつ飽和するかもわからないコアをギリギリで使っているからです。電源短絡を恐れながら使う回路なんて、FETがいくつ焼けるかわかりません。なので、なんとかして電流を制限する必要がありそうです。現在(4/29)、コロナの影響で部活動が原則禁止で、部室の機材を好き放題使う借りることができない状況です。家でバカスカ電流を流すような回路を扱うのは嫌なので、シミュレータ上でいろいろ試すことにしました。みんなだいすきLTSpiceです。
いろんな昇圧回路を調べたのですが、解決のヒントはあるブログで見つけた400W級昇圧チョッパから得ました。それがこちら。実際に作る回路とは多少異なりますが仕組みは同じです。



f:id:Agchan_Luice:20210429182653j:plain
やばくなったら止める。以上。
X1とX2はハーフブリッジで、互い違いに動かしてフルブリッジを作ってます。シャント抵抗がやや大きいのは、小さいのを使おうとしてノイズのせいで痛い目見たことがあるからです。SRラッチとコンパレータはPICのペリフェラルに入ってるのでそれで処理します。入力にローパス入れる必要が出てくるかもしれません。
シミュレータ上の回路はこちら。




f:id:Agchan_Luice:20210429183133p:plain
トランスのインダクタンスは適当です。2:1だったらでかすぎなきゃいいです。2次側でいちいちダイオードブリッジを組んでいるのは、2次側にセンタータップがないからです。線材細くすればよかったのに。
参考元の回路は非絶縁のチョッパで、最大電流と最小電流を監視していたのですが、こちらは最大電流のみを監視します。これで電流流しすぎは防げるはず。もっとも、設定した最大電流を流す前にコアが飽和する可能性があるんですけどね。




f:id:Agchan_Luice:20210429183606p:plain
青:1次コイル電流
緑:出力電圧

出力にはとりあえず36Ωがつないであります。
とりあえず1次側の電流を2Aに制限することは出来たのですが、2次側の電圧を見てみると5Vギリギリって感じです。まあこれは結合もインダクタンスもよくわかっていないので実際に試さないと何とも言えません。
100kHzで発振してますが、500mA流した時で25kHzくらいだったのでその時より周波数は下がるはずです。おそらくスイッチングも間に合うでしょう。
ただ、この回路はフルブリッジを組むのがすごくめんどくさいです。マイコンも使ってていろいろめんどくさそうなので、ほかの方法も考えてみることにします。



沼った

絶縁DCDCには複数の方法があり、別の方法を試してみたものの、そっちはそちらで沼に嵌ることになりました。趣旨とだいぶずれていきそうなので、また別に書くことにします。つづく。

ニキシー管用の電源回路

高電圧、作るだけなら容易い

昇圧回路を作るべく試行錯誤していたところで、お土産にとたまたまニキシー管をもらいました。ニキシー管は高電圧が必要ですが電流量は皆無に等しいので、適当にチョッパを組むだけで作れます。要領はこの前の降圧回路と変わりません。
f:id:Agchan_Luice:20210426210333j:plain
f:id:Agchan_Luice:20210426210317j:plain
キレイダナー

回路図

f:id:Agchan_Luice:20210426205634j:plain

昇圧電源のみの回路図です。スイッチングはFETじゃないの?と思われるかもしれませんが、その辺にあった200V近い耐圧のあるスイッチング素子は唯一2SK5200だけでした。出力電圧を分圧してフィードバックしています。120kΩを4パラしているのがブレボの画像を見てもわかりますが、こうして30kΩを作らないと、単体でやると定格オーバーどころか焦げ始めます。まあ4パラしてもぎりぎり定格オーバーなんですけどね。
ニキシー管はアノードコモンなので、一か所に40kΩくらいを通して180Vを供給してやればあとはほかのピンをGNDに落とすだけで光ります。なので、ブレボ上にあるDIPスイッチで、手で切り替えていました。

大容量の昇圧電源、どうやって作るんだろうなあ.....

スイッチング降圧電源回路

買えないものは作ります

普段何気なく部室で使っている電源装置、あれって実は結構なお値段がするんですよね。でも家で電子工作をするには電源装置は必須です。なので、作ることにしました。友人が日本橋に行ったときにAC100Vを18VDCにする電源(300円)を買ってきてもらっていたので、ここからさらに18V~0Vに変換する降圧回路をつくりました。電流の消費が少なければ、12VくらいのDCジャックからの給電で動くと思われます。なんならDCジャックじゃなくても、AC100Vをトランスで12Vくらいまで落としてからブリッジで整流、コンデンサで平滑すればたぶん動きます。あんまりやりたくないけど。
そんなに電流たくさん消費するような工作を家ではしたくないので、2Wも出れば十分です。

f:id:Agchan_Luice:20210425102145j:plain
300円のスイッチング電源

ディスクリートのロマン

既存の電源素子を買って指示通りに作るのもいいのですが、それではおもしろくないので、専用の素子を使わずにオペアンプトランジスタとコンパレータを使って、降圧チョッパを制御してみました。マイコンももちろん使っていません。
設計ミスで基盤のサイズを間違えたので、上下に無理やり基盤を拡張しています。
f:id:Agchan_Luice:20210425225635j:plainf:id:Agchan_Luice:20210425225647j:plain






全体の回路図と配置図
f:id:Agchan_Luice:20210425104303j:plainf:id:Agchan_Luice:20210425225317p:plain

主にこれら4つの回路で構成されています。
・降圧チョッパ
PWM制御回路
・のこぎり波発生回路
・負電圧発生回路


それぞれ説明していきます。


・降圧チョッパ
f:id:Agchan_Luice:20210425211415p:plain
降圧チョッパのテンプレ型です。Q6がターンオンすると、コイルがエネルギーを貯めつつ2次側に電流を供給します。Q6をターンオフすると、コイルへのエネルギーの供給が止められ、コイルがエネルギーを放出し、これをC11で均します。スイッチングはNchMosFETなので、適当にチャージポンプを乗っけています。出力電圧がVGSより小さいので、ターンオフしたときの電位差のせいで定格(±20V)オーバーなんてことはきっとないはず。たぶん。




PWM制御回路
f:id:Agchan_Luice:20210425212649p:plain
ただスイッチングするだけでは負荷変動時に電圧が大きく変化するので、スイッチングするPWM信号のduty比を制御をする必要があります。duty比が高いと出力電圧は上がります。なので、オペアンプで出力電圧と目標電圧を差動増幅し、その出力とキャリア波をコンパレータに突っ込んでPWM信号を生成します。画像中のfbが出力で、3ピンコネクタには可変抵抗がつながっています。比例ゲインは適当です。5V出力時に無負荷~1Wでも、リプルは0.5V程度でした。




・のこぎり波発生回路
f:id:Agchan_Luice:20210425212725p:plainf:id:Agchan_Luice:20210425214439p:plain
定電流でコンデンサを充電すると電圧が線形増加するので、これを利用しました。カレントミラー回路で定電流を作っています。オペアンプとコンパレータで作ったPWM信号をパルス回路に入力し、そのパルス信号でトランジスタを駆動してコンデンサを放電します。これによって、コンデンサの電圧がのこぎり波状になります。充電電圧を監視し、ヒステリシスコンパレータで放電でも良かったのですが、オペアンプも余っていたしこっちの方が周波数の調整が簡単なのでこの方法をとりました。




・負電圧発生回路
f:id:Agchan_Luice:20210425223728p:plain
オペアンプとコンパレータの電源に負電圧が必要なので搭載しています。無安定マルチで適当に発振を起こして、チャージポンプ回路で-5Vを作っています。三端子レギュレータから5Vを作っているのですが、どうせ5Vはほしくなることが多いだろうということで、出力のコネクタも設けました。




回路剝き出しだと普通に危ないので、ガワをMDFか何かで作ろうかと思います。

マインクラフトは手ごろなCPUエミュ

ロマンを求めて

CPUを自作してみたいと思ったら、ロジックICを組み合わせて作るのが普通かと思うのですが、そこまでするお金と気力がありませんでした。そこで使ったのがマインクラフトというゲームです。もとよりさんざん遊んでいたこのゲーム、実はゲーム内で論理回路を作ることができます。論理回路というよりは順序回路ですが。
ということで、ゲーム内でCPUを作ってみました。いつか作ってみたいと思っていたんですよね。
全体像がこちら。
f:id:Agchan_Luice:20210418205757p:plain
4bitアーキテクチャです。とりあえず一個作ろうと思って作ったものです。一回作るとコツが見えてくるので、二回目またすぐ作ることにしました。それがこちら。
f:id:Agchan_Luice:20210418211514p:plain
こちらは8bitアーキテクチャです。明らかに巨大化しています。一つ目は制作に一週間かかったのに、2つ目は2~3日程度で完成しました(結局はコピペで同じモジュールの量産なので規模自体はあんまり関係ない)。仕組みの解説はこっちでやります。一個目はアーキテクチャがあまりにもアレだったので...
2個目の方のアーキテクチャがこちら。図が汚いけど我慢してください。
f:id:Agchan_Luice:20210418220404p:plain
とりあえず動けばよいという構成です。

仕組み

CPUを構成している部品は主にこれら8つのユニットです。

・プログラムメモリ
・命令カウンタ
・命令デコーダ
・Wレジスタ
・RAM
・ALU
・条件分岐
・GPIO(General Purpose Input/Output)


一つづつ解説します。

・プログラムメモリ
f:id:Agchan_Luice:20210418211506p:plain
一番サイズ的に大きなユニットです。いわゆる機械語をここに順番に書きこんでいきます。1命令14bitとなっています。00011010011010みたいな感じで並べていきます。レッドストーントーチというブロックを手作業で一つづつ設置してビットを立ち上げていく必要があるので、プログラムの書き込みはかなりの重労働です。おかげでこのCPUは機能としてはいろいろできる割に、書き込むのが大変すぎて一番長いコードでも8byte型の足し算くらいしかしていません。かわいそう




・命令カウンタ
f:id:Agchan_Luice:20210418214902p:plain
プログラムメモリから読み出す命令のアドレスをカウントする部分です。ジャンプ命令が来ると、指定のアドレスまで飛びます。また、条件分岐の命令で評価が真だった場合は条件分岐のモジュールから信号が飛んできて、それをトリガにして命令を1つ飛ばします。飛んだ先にジャンプ命令を置いておくことで、if文みたいな動作をできます。




・命令デコーダ
f:id:Agchan_Luice:20210418211451p:plain
プログラムメモリから吐き出された命令を各モジュールへと分配する回路です。要はただのデマルチプレクサなわけですが。上3桁の数値をアドレスにして分けています。




・Wレジスタ
f:id:Agchan_Luice:20210418213928p:plain
いわゆるレジスタです。RとLとの二つの1byte分のメモリがあり、それぞれ読み書きができます(以後WR、WLと表記)。これがモジュール間でデータをやりとりするためのバッファとなっています。



・RAM
f:id:Agchan_Luice:20210418221603p:plain
ランダムアクセスメモリ、通称RAMです。よくPCのスペックに書かれているアレです。CPUの部品の一つとして数えるのはちょっと違うような気もしますが必ず必要なので。WRもしくはWLの値を読んで格納したり、またWRもしくはWLに書き込めます。容量はなんと16byte分しかありません。これでも一個目のCPUより4倍容量が増えたんです。




・ALU
f:id:Agchan_Luice:20210418211415p:plain
ALUはArithmetic and Logic Unit(算術論理演算装置)の略で、CPUが実際に計算を行う部分です。Wレジスタの値を使って計算します。結果はWRに格納されます。機能は以下7つです。

・NOT
・AND
・OR
・XOR
・右シフト
・左シフト
・加算

シフトや加算でオーバーフローが発生すると、WLの最下位ビットが立ち上がります。




・条件分岐
f:id:Agchan_Luice:20210418222701p:plain
いわゆるif文を実現するための回路です。Wレジスタの値を使って評価します。使える条件は以下3つです。

・WRとWLの完全一致
・WRが正か負か(最上位ビットが立ち上がっているか否か)
・0以外か

評価した結果が真になると、前述したように命令カウンタに信号が送られて、一つ命令が飛ばされます。偽だった場合には何も起こりません。




・GPIO(General Purpose Input/Output)
f:id:Agchan_Luice:20210418223808p:plain
GPIOは汎用入出力ポートです。指定した番号の入力ポートの値がWRに格納できます。また、指定した番号の出力ポートにWRの値を出力できます。これで外部との値のやり取りが可能になります。その気になればディスプレイとかキーボードを接続とかも可能ではあるのですが、動作がめちゃくちゃ遅いのは自明です。


動かすだけなら意外とチョロい

やってることは複雑そうに見えますが、結局はいろんなモジュールに順番に命令を送っているだけのことです(マサカリが飛んできそう)。ただ、単純なCPUには単純なことしかできないわけです。複雑なことをやろうとしたらかなり制約が出ます(主に速度が)。近頃のパソコンのCPUのアーキテクチャはかなり複雑化しているので、我々常人には理解できないかと思われます。しかしいくら複雑なCPUだって、中で動いているのはCMOSでできたANDやORやNOTです。
総評:PCってすごい。(小並感)
ではこの辺で。

気が付いたらオセロAIを書くことになっていた話

言い出しっぺが責任を持ちます

たぶん去年、授業でPythonを扱うことになり、そのうちにグループで何かしらを作って発表することになったことがありました。軽率に「オセロでもやらね?」って言ったら本当にオセロをやることになり、言い出しっぺである僕がAIを書くことになりました。最初は「まあちょろいだろ」って思ってたけど、軽く調べたら不安になるくらい難しいことばかり書いてありました。もうどうでもよくなったので(よくない)、とりあえず思い付きのコードで実装してみた。そしたらなんかそれなりに上手いこと動いたのでよかった(ほんまか?)。丸一日かけて書いたからしんどかった記憶がある。
自分の担当はAIだけだったので、手順決めや表示等はほかの人がやっています。他人のコードを晒すわけにはいかないので、自分が書いたAIとオセロのシステムのコードだけ公開します。ソースコードは以下2つ。

・example.py:使用例
・Reversi.py :システムとAI本体

ソースコード

example.py

import Reversi

#インスタンス生成
a = Reversi.reversi(30,8,'black')

while(1):
    #盤面表示
    a.ShowBoardPic(a.table)

    #プレイヤーの手番
    a.PlayerTurn()

    #ゲーム終了かどうか確認
    if a.EndGameCheck():break
    #AIの手番
    a.AITurn()
    #ゲーム終了かどうか確認
    if a.EndGameCheck():break

Reversi.py

import sys
import copy
import random
from PIL import Image,ImageDraw
import re


#角の追加点
edge_additional = 10

#敵の角の減点
enemy_edge_additional = -10

#辺の追加点
side_additional = 1

#角のL字の追加点
edgel_additional = 3

#外より位置マス内側の減点
outline_additional = -3

#未使用
sideenemy_additional = -3

class reversi:
    lv = 1
    table = [[]]
    n = 8
    side = 0
    
    isVisiblePoint = False


    def __init__(self,level:int,size:int,AIside:str):
        #引数エラー処理群
        if(level <= 1):
            print("Error:Make sure the level is 1 or higher.")
            sys.exit()
        if(size % 2 == 1):
            print("Error:Make sure size is even number.")
            sys.exit()
        if(4 > size):
            print("Error:Make sure minimum size is 6.")
            sys.exit()
        isTrueSide = False
        if(AIside == 'black'):isTrueSide = True
        if(AIside =='white'):isTrueSide = True
        if isTrueSide == False:
            print("Error:Make sure AI side is \"black\" or \"white\".")
        
        #各情報出力
        print("AI level is",level,"\nsize is",size,"×",size)
        
        #各値代入
        self.n = size
        self.lv = level
        if(AIside=='white'):self.side = 1
        if(AIside=='black'):self.side = -1
        self.table = [[0] * size for i in range(size)]
        
        #テーブル初期化 白1 黒-1
        for i in range(self.n):
            for j in range(self.n):
                if (i == size / 2 - 1)&(j == size / 2 - 1):
                    self.table[i][j] = 1
                    self.table[i][j+1] = -1
                    self.table[i+1][j] = -1
                    self.table[i+1][j+1] = 1


    def VisiblePoint(self,a:bool):self.isVisiblePoint = a

    #盤面の様子をjpg出力
    def GenerateBoardPic(self,name:str,board:list):
        im = Image.new('RGB', (self.n * 100, self.n * 100), (0, 128, 0))
        draw = ImageDraw.Draw(im)
        
        for i in range(self.n - 1):
            draw.line(((i + 1) * 100,0,(i + 1) * 100,self.n * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n - 1):
            draw.line((0,(i + 1) * 100,self.n * 100,(i + 1) * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n):
            for j in range(self.n):
                if(board[i][j]==1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(255,255,255),outline=(0,0,0))
                if(board[i][j]==-1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(0,0,0),outline=(0,0,0))
        
        for i in range(0,self.n):
            for j in range(0,self.n):
                draw.text((i * 100 + 40,j * 100 + 40),str((i,j)),fill=(128,0,0))
        
        im.save(name)

    #盤面の様子を標準機能で表示
    def ShowBoardPic(self):
        board = self.table
        im = Image.new('RGB', (self.n * 100, self.n * 100), (0, 128, 0))
        draw = ImageDraw.Draw(im)
        
        for i in range(self.n - 1):
            draw.line(((i + 1) * 100,0,(i + 1) * 100,self.n * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n - 1):
            draw.line((0,(i + 1) * 100,self.n * 100,(i + 1) * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n):
            for j in range(self.n):
                if(board[i][j]==1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(255,255,255),outline=(0,0,0))
                if(board[i][j]==-1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(0,0,0),outline=(0,0,0))
        
        for i in range(0,self.n):
            for j in range(0,self.n):
                draw.text((i * 100 + 40,j * 100 + 40),str((i,j)),fill=(128,0,0))
        
        im.show()


    def OneLinerFromAdditionalPoint(self,s:int,line:list):
        isToSearch = True
        if len(line) < 3:isToSearch = False
        if(len(line)!=1):
            if line[1] != s * (-1):isToSearch = False
        
        ans = 0
        if isToSearch:
            #指定色基準に反転
            for i in range(0,len(line)):
                line[i] *= s
        
            #追加点の計算
            for i in range(1,len(line) - 1):
                #0以外,0の並びが見つかったら追加点0にしてやめる
                if (line[i]!=0)&(line[i + 1] == 0):
                    ans = 0
                    break
                    
                #-1,1の並びを探す
                ans += 1
                if(line[i]==-1)&(line[i + 1] == 1): break
                #最後まで移動したら追加点0
                if i==len(line) - 2:
                    ans = 0
                    break
        return ans

    #配置可能な場所か確認(0) 返り値:(追加取得マス数,[方向])
    def isTruePos(self,x:int,y:int,board:list,col:str):
        #引数確認
        if(x > self.n - 1):return [0,None]
        if(y > self.n - 1):return [0,None]
        
        
        s = 0
        if(col == 'white'):s = 1
        if(col == 'black'):s = -1
        if(s==0):return [0,None]
        
        #既におかれているなら0
        if(board[x][y] != 0) : return [0,None]
        
        #総計用
        AddPoint = 0
        
        #方向
        direction = []
        
        #右方向
        line = []
        for i in range(x,self.n):
            line.append(board[i][y])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('right')
        
        #左方向
        line = []
        for i in range(0,x + 1):
            line.append(board[x-i][y])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('left')
        
        #上方向
        line = []
        for i in range(0,y + 1):
            line.append(board[x][y-i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('upper')
        
        #下方向
        line = []
        for i in range(y,self.n):
            line.append(board[x][i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('downer')
        
        #右斜め上方向
        line = []
        for i in range(0,self.n):
            if((x + i > self.n - 1)|(y - i < 0)):break
            line.append(board[x + i][y - i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('right_upper')
        
        #右斜め下方向
        line = []
        for i in range(0,self.n):
            if((x + i > self.n - 1)|(y + i > self.n - 1)):break
            line.append(board[x + i][y + i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('right_downer')
        
        #左斜め上方向
        line = []
        for i in range(0,self.n):
            if((x - i < 0)|(y - i < 0)):break
            line.append(board[x - i][y - i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('left_upper')
        
        #左斜め下方向
        line = []
        for i in range(0,self.n):
            if((x - i < 0)|(y + i > self.n - 1)):break
            line.append(board[x - i][y + i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('left_downer')
        
        return [AddPoint,direction]


    def TablePoint(self,x:int,y:int,board:list,col:str):
        t = 0
        myside = 1 if col == 'white' else -1
        enemyside = -myside
        
        #数
        for i in range(0,self.n):
            for j in range(0,self.n):
                if board[i][j] == myside:
                    t += 1
        
        #辺
        for k in range(0,self.n):
            if board[0][k] == myside:
                t += side_additional
        for k in range(0,self.n):
            if board[k][0] == myside:
                t += side_additional
        for k in range(0,self.n):
            if board[self.n - 1][k] == myside:
                t += side_additional
        for k in range(0,self.n):
            if board[k][self.n - 1] == myside:
                t += side_additional
        
        #角
        if board[0][0] == myside:t += edge_additional
        if board[self.n - 1][0] == myside:t += edge_additional
        if board[0][self.n - 1] == myside:t += edge_additional
        if board[self.n - 1][self.n - 1] == myside:t += edge_additional
        
        #辺より1マス内側に置こうとする場合
        if x == 1:t += outline_additional
        if y == 1:t += outline_additional
        if x == self.n - 2:t += outline_additional
        if y == self.n - 2:t += outline_additional
        
        #敵が角を取った場合
        if board[0][0] == enemyside:t += enemy_edge_additional
        if board[self.n - 1][0] == enemyside:t += enemy_edge_additional
        if board[0][self.n - 1] == enemyside:t += enemy_edge_additional
        if board[self.n - 1][self.n - 1] == enemyside:t += enemy_edge_additional
        
        return t


    def MaxPoint(self,board:list,col:str):
        pos = []
        index = 0
        enemyside = -1 if col == 'white' else 1
        myside = -enemyside
        
        maximum = [0,0,-999]
        
        for i in range(0,self.n):
            for j in range(0,self.n):
                t = self.isTruePos(i,j,board,col)[0]
                if t == 0:continue
                t = 0
                for k in range(0,self.n):
                    for l in range(0,self.n):
                        if self.table[k][l] == myside:t += 1
                
                #得点計算用盤面
                tmpb = copy.deepcopy(board)
                self.BoardUpdate(i,j,col,tmpb)
                
                t += self.TablePoint(i,j,tmpb,col)
                
                pos.append([i,j,t])
                if index == 0:maximum = [i,j,t]
                index += 1
        
        
        for i in range(0,len(pos)):
            if(pos[i][2]>maximum[2]):maximum = pos[i]
        
        return maximum


    def FlipOnLine(self,line,col):
        s = 1 if col == 'white' else -1
        for i in range(len(line)):
            #同色がきたら抜ける
            if line[i] == s:break
            #異色がきたら反転
            if line[i] == -s:line[i] = s
        return line


    def BoardUpdate(self,x,y,col:str,board:list):
        p = self.isTruePos(x,y,board,col)
        if(p[0] < 1):return
        
        for s in p[1]:
            if s== 'right':
                line = []
                for i in range(x,self.n):
                    line.append(board[i][y])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x + i][y] = line[i]
            
            if s == 'left':
                line = []
                for i in range(0,x):
                    line.append(board[x - i][y])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x - i][y] = line[i]
            
            if s == 'upper':
                line = []
                for i in range(0,y):
                    line.append(board[x][y - i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x][y - i] = line[i]

            if s == 'downer':
                line = []
                for i in range(y,self.n):
                    line.append(board[x][i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x][y + i] = line[i]
            
            if s == 'right_upper':
                line = []
                for i in range(0,self.n):
                    if((x + i > self.n - 1)|(y - i < 0)):break
                    line.append(board[x + i][y - i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x + i][y - i] = line[i]
            
            if s == 'right_downer':
                line = []
                for i in range(0,self.n):
                    if((x + i > self.n - 1)|(y + i > self.n - 1)):break
                    line.append(board[x + i][y + i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x + i][y + i] = line[i]
            
            if s == 'left_upper':
                line = []
                for i in range(0,self.n):
                    if((x - i < 0)|(y - i < 0)):break
                    line.append(board[x - i][y - i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x - i][y - i] = line[i]
            
            if s == 'left_downer':
                line = []
                for i in range(0,self.n):
                    if((x - i < 0)|(y + i > self.n - 1)):break
                    line.append(board[x - i][y + i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x - i][y + i] = line[i]
        
        #指定地点に自色を置く
        board[x][y] = 1 if col=='white' else -1


    def NextMaxPointPos(self,board:list,col:str):
        enemycol = 'black' if col == 'white' else 'white'
        
        pos = []
        
        index = 0
        
        #候補地に設置
        for ai in range(0,self.n):
            for aj in range(0,self.n):
                atmp = copy.deepcopy(board)
                a = self.isTruePos(ai,aj,atmp,col)[0]
                if a == 0:continue
                self.BoardUpdate(ai,aj,col,atmp)
                
                #敵の最善手で進める
                epos = self.MaxPoint(atmp,enemycol)
                self.BoardUpdate(epos[0],epos[1],enemycol,atmp)
                
                #次の手の得点を算出
                ptmp = self.MaxPoint(atmp,col)
                
                #得点と座標を格納
                pos.append([ai,aj,ptmp[2] - epos[2]])
        
        #得点で昇順ソート
        for i in range(0,len(pos) - 1):
            for j in range(i,len(pos)):
                if pos[i][2] < pos[j][2]:
                    tmp = pos[i]
                    pos[i] = pos[j]
                    pos[j] = tmp
        
        #同得点のものからランダム選出
        mp = pos[0][2]
        index = 0
        for i in range(1,len(pos)):
            if mp != pos[i][2]:
                break
            index += 1
        
        for i in range(0,index):
            a = random.randint(0,index - 1)
            b = random.randint(0,index - 1)
            tmp = pos[a]
            pos[a] = pos[b]
            pos[b] = tmp
        
        
        return (pos[0][0],pos[0][1])


    def isAbleToPut(self,board:list,col:str):
        for i in range(0,self.n):
            for j in range(0,self.n):
                if self.isTruePos(i,j,board,col)[0] != 0:return True
        return False


    def ReadLvLoop(self,level,board:list,col:str):
        #一手先を出す
        pos = self.NextMaxPointPos(board,col)
        
        if self.isAbleToPut(board,col):
            #これ以上先を読めなければ、座標を返して再帰ループから抜ける
            return (pos[0],pos[1])
        
        #一手先の盤面をつくる
        tmpb = copy.deepcopy(board)
        self.BoardUpdate(pos[0],pos[1],col,tmpb)
        
        if level == 1:
            #level - 1回繰り返したら次の手を返して再帰ループから抜ける
            return (pos[0],pos[1])
        
        print(level)
        #一手先の盤面を現在の盤面、level - 1として再帰呼び出し
        return self.ReadLvLoop(level - 1,tmpb,col)

#AIのターン
    def AITurn(self):
        self.EndGameCheck()
        mycol = 'white' if self.side == 1 else 'black'
        if self.isAbleToPut(self.table,mycol) == False:
            print('AI passed')
            return
        
        fs = self.ReadLvLoop(self.lv,self.table,mycol)
        self.BoardUpdate(fs[0],fs[1],mycol,self.table)
        
        print(fs)
        
        if self.isVisiblePoint == True:
            a = self.TablePoint(fs[0],fs[1],self.table,mycol)
            print(a)

#プレイヤーのターン
    def PlayerTurn(self):
        self.EndGameCheck()
        mycol = 'white' if self.side == -1 else 'black'
        if self.isAbleToPut(self.table,mycol) == False:
            print('You passed')
            return
        
        x = 0
        y = 0
        
        while(1):
            print('x y')
            tmp = input()
            
            chk = re.search(r'\d\s\d',tmp)
            
            if chk == None:
                print("Invalid!")
                continue
            
            i = chk.group(0).split()
            
            x = int(i[0])
            y = int(i[1])
            
            a = self.isTruePos(x,y,self.table,mycol)[0]
            if a == 0:
                print("Invalid!")
                continue
            break
        
        
        self.BoardUpdate(x,y,mycol,self.table)
        
        if self.isVisiblePoint == True:
            a = self.TablePoint(x,y,self.table,mycol)
            print(a)

#ゲームが終了したかどうか判定
    def EndGameCheck(self):
        if (self.isAbleToPut(self.table,'white') == False) & (self.isAbleToPut(self.table,'black') == False):
            print('GAME OVER!')
            w = 0
            b = 0
            for i in range(0,self.n):
                for j in range(0,self.n):
                    if self.table[i][j] == 1:
                        w += 1
                    if self.table[i][j] == -1:
                        b += 1
            print('WHITE:',w,' BLACK:',b)
            return True
        return False

3,2\n とか打つと、左から4つ目、上から3つ目の位置に自分の色を置けます。
このまま動かすと、デバッグ用に用意した盤面の画像が別窓で表示されます。画像データだけを出力もできるので、tkinterとかで適当にGUIに表示するのがいいかもしれません。

仕組み

AIのカラクリとしては割と単純で、盤面の状態を点数化して、その点数が高くなるように指していきます。点数は、盤面に自分の色が増えるほど高くなり、また角や辺をたくさん取れるとより点数が高くなるようにしています。AIは、プレイヤーも常に点数が高くなるように指してくると想定して盤面を読み進めます。これをすべてのマスに対して行い、一定手数先まで読んだときに一番点数が高かったマスに指します。レベルの数値は、この先読みする手数となっています。

まとめ

クラスメイトを数人ボコボコにできたので非常に満足です。
2つのシステムで盤面を共有できるので、やろうと思えばプレイヤー vsプレイヤーとか、AI vs AIもできます。