2020年8月25日 星期二

android 用 kotlin coroutine 語法測試 LocalSocket

Java 有內建 LocalSocket 及 LocalServerSocket 兩種類型的程式庫, 是作為內部進程溝通管道(IPC), 進程甚至包括用 c 語言寫的外部 JNI 程式都可以, 一般的 client 就是 LocalSocket 類型 , 若要當伺服器時用 LocalServerSocket , 延續上一篇, 同樣採用 coroutine 方式:
    import kotlinx.coroutines.*
    import android.net.LocalSocket
    import android.net.LocalServerSocket
    import android.net.LocalSocketAddress
    import java.io.IOException 

    private val localSocketName = "com.example.testLocalSocket"   
    private suspend fun localServerListen() {
        val localServerSocket = LocalServerSocket(localSocketName) 
        while(true) {
            try {
                println("server listen...")
                val localSocket = localServerSocket.accept()// will block to listen
                println("server accept $localSocket")
                val array32 = ByteArray(32)
                val len = localSocket.getInputStream().read(array32)// will block to read
                println("server receive:")
                for (x in 0 until len) print("${array32.get(x)},")           
                println("\nserver finish: $len")
                localSocket.close()
            } catch (e: IOException) {
                e.printStackTrace();
                break;
            }
        }
        localServerSocket.close();
    }
    private suspend fun testLocalSocket(start:Int = 100) {
        try {
            val localSocket = LocalSocket()
            val endpointServer = LocalSocketAddress(
                localSocketName,
                LocalSocketAddress.Namespace.ABSTRACT
            )
            println("client connect ...")
            localSocket.connect(endpointServer)
            println("client send: $localSocket")
            val len = 32
            val array32 = ByteArray(len)
            for (x in 0 until len) array32.set(x, (start + x).toByte())
            localSocket.getOutputStream().write(array32, 0, len)
            localSocket.close()
        } catch (e: IOException) {
            e.printStackTrace();
        }
    }
在主程序用 GlobalScope.launch 同時發動三條 coroutines 一起執行:
    GlobalScope.launch(Dispatchers.IO) {
        launch { localServerListen() }
        launch { testLocalSocket(90) }
        launch { testLocalSocket(80) }
    }


後記: 用 c 寫 LocalSocket 通信程式, 使用 Unix Socket_Stream,並採用"虛根"的命名空間 (abstract namespace):
1. // localSocket.h   used  in  client  and  server
  char localSocketName[ ] = "com.example.testLocalSocket";// strlen < 106

2. // localSocket.c    for   client
  #include <stdlib.h>
  #include <stdio.h>
  #include <stddef.h>
  #include <sys/socket.h>
  #include <sys/un.h>
  #include <string.h>
  #include <unistd.h>
  #include "localSocket.h"
  int main() {
        int localSocket = socket(AF_UNIX, SOCK_STREAM, 0);
        if (localSocket < 0) exit(1);           
        struct sockaddr_un endpointServer; // socket for loopback addr
        memset(&endpointServer, 0, sizeof(endpointServer));// init  
        endpointServer.sun_family = AF_UNIX;
        int sunOffset = offsetof(struct sockaddr_un, sun_path);
        endpointServer.sun_path[0]  = '\0';
        strcpy(endpointServer.sun_path + 1, localSocketName);     
        socklen_t len = 1 + strlen(localSocketName) + sunOffset;

        if(connect(localSocket, (struct sockaddr *)&endpointServer, len) >= 0) {
            char msg[] = "Hello";
            send(localSocket, msg, strlen(msg), 0);
        }
        close(localSocket);
    }

3. // localServerSocket.c   for   server
    #include <stdlib.h> 
    #include <stdio.h> 
    #include <stddef.h> 
    #include <sys/socket.h> 
    #include <sys/un.h> 
    #include <string.h> 
    #include <unistd.h> 
    #include "localSocket.h"
    int main() {
        int localSocket = socket(AF_UNIX, SOCK_STREAM, 0);
        if (localSocket < 0) exit(1);                   
        struct sockaddr_un endpointServer; // socket for loopback addr
        memset(&endpointServer, 0, sizeof(endpointServer));// init   
        endpointServer.sun_family = AF_UNIX;
        int sunOffset = offsetof(struct sockaddr_un, sun_path);          
        endpointServer.sun_path[0]  = '\0';
        strcpy(endpointServer.sun_path + 1, localSocketName);    
        socklen_t len = 1 + strlen(localSocketName) + sunOffset;
       
        if (bind(localSocket, (struct sockaddr *)&endpointServer, len) >= 0 && listen(localSocket, 5) >= 0) {
            int socketfd = accept(localSocket, (struct sockaddr *)&endpointServer, &len);
            if(socketfd >= 0) {
                char buf[1024];
                int n = read(socketfd, buf, sizeof(buf));
                buf[n] = 0;
                printf("server receive: %s\n", buf);
                close(socketfd);
            }
        }
        close(localSocket); 
    }

編譯上述兩個程式並執行看看:
    g++  localServerSocket.c   -o   server   &&   ./server  &
    g++  localSocket.c   -o  client   &&   ./client

理解"虛根"(abstract root)的用意:上述 endpointServer.sun_path[0] = '\0'一開頭處填入  0 (零, 也是字串結尾 End Of String), 會讓 bind localSoket 時, 不用在檔案系統內產生檔案,否則執行的程式將在檔案系統目錄內建構出一個 domain name(長度為 0 的檔案 com.example.testLocalSocket),導致不同目錄下執行的程式將有不同的 domain name(整個 domain name 包含前置目錄,除非在同根的目錄用相同名稱,才會有相同 domain name), 執行 domain name 各異的 LocalSocket 程式,除非在命名上取巧,基本上是無法相互溝通的.LocalSocket 採用"虛根"是一個很聰明的方式,可想像 0 就是一個虛根,當執行的程式在虛根上長出 domain name,也就不需在檔案系統下留下任何軌跡,LocalSocket 通信程式只要在虛根上採用了相同名稱,自然就能相互通訊,解決了 domain name 命名的麻煩.

android 用 kotlin coroutine 語法測試 Socket

以往使用 tcp Socket 通信免不了要用 Thread 處理非同步的工作(async  task), 但有了  couroutines 程式庫後就容易多了, 但要注意的是 Android 不能在主程(MainThread 又稱為 UI Thread)使用網路的功能,這時就要用派任工 Dispatchers.IO 分配到後台去執行, Java 有內建 Socket 及 ServerSocket 兩種類型的程式庫, 若要當 tcp  伺服器時用 ServerSocket , 一般的 tcp client 就是 Socket 類型:

    import kotlinx.coroutines.*
   //  import java.net.ServerSocket // tcp server class
    import java.net.Socket  // tcp client class

    private suspend fun testSocket()  {
        try {
            val socket  = Socket("www.google.com.tw", 443) // TCP socket for client
            val connect = socket.isConnected()
            println("$socket : $connect")
            socket.close()
        } catch (e: Exception) {
            println("Socket Error@testSocket: $e")
        }
    }
主程序用 GlobalScope.launch 直接發動 coroutines:
    GlobalScope.launch(Dispatchers.IO) {                      
        launch {
            testSocket()
        }
        // ...
    }

備註:
AndroidManifest.xml 添加網路授權:
          <uses-permission android:name="android.permission.INTERNET" />
build.gradle 添加 kotlin coroutines 程式庫:
     dependencies {
         implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
         implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
         implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
     }


2020年8月20日 星期四

採用 Android Kotlin 程式庫的 NSD 網路服務先探

參考資料: https://developer.android.com/training/connect-devices-wirelessly/nsd

linux mint 上針對區域網路的名稱服務協定可以使用 avahi, 透過 avahi-browse -a  -r 就能探聽到區域網路上服務裝置所丟出的服務訊息,從而解析其 ip 位址及所接受的服務端口 port, 他支援常見的名稱服務協定 mDNS/DNS-SD, Android 裝置上使用的 NSD 也支援 mDNS/DNS-SD

這個服務協定原理是:由服務裝置先在區域網路上用 multicast 群組廣播它所服務的名稱類型,區域網路上有興趣者,擷取到該訊息或事先知道訊息就可以透過相同管道去探尋,想辦法獲得該服務裝置的 ip  位址與通信端口 port, 之後通過 ip:port 直接與對方一對一(p2p)通訊, 像這樣透過 p2p (peer to peer)的技術, 讓 app 自成一套網路, 不用伺服器,而讓 app 身兼伺服器角色, 讓ㄧ對多或是多對多通信也不成問題.

 Android 的服務裝置透過 NsdManager 就能完成包含服務公告及探索任務, 使用 kotlin 來寫程式好處是不用像 Java 要寫長長的程式碼, 善用 coroutine 也能完成非同步任務(async task), 首先編輯在 app 底下的 build.gradle, 加入支援 kotlinx-coroutines 的程式庫:
    dependencies {
        implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

    }

在程式內也必須引入所用到的相關定義:
    import android.content.Context
    import android.net.nsd.NsdManager
    import android.net.nsd.NsdServiceInfo

    import kotlinx.coroutines.*
    // ...
    val _nsdManager = context.getSystemService(
                Context.NSD_SERVICE) as NsdManager


當 kotlin 的 fun 函式前面冠上保留字 suspend 時, 在裏面就能呼叫別的 suspend 函數(有點類似 javascript 的 function 前冠上 async 就能在函式裏 await 呼叫別的 async function 類似的道理), Kotlin 會將他編譯成一個可以暫停/重啟/毀滅的狀態機(state machine)函式, kotlin 程式庫裏面有一個 suspend fun delay(毫秒單位) 可以用來暫停一段時間同時切換到下一個任務繼續執行, suspend fun 函式一旦呼叫了其它 suspend fun, 函式本身將招致凍結, 等到所呼叫的函式完成後就會再度被喚醒, 用回圈定時輪詢的方式來實現一個 NSD 公告註冊程序:
    private suspend fun nsdRegisterTCP (
         protocol:Int="http",
         servicePort: Int=8080
    ): String {
        val nsdInfo = NsdServiceInfo( ).apply {
            setServiceName("MyPhone") // 名稱自由指定, 有可能會被 android 修改
            setServiceType("_$protocol._tcp") // 協定用 _xxx._xx 方式命名
            setPort(servicePort) // 公告的服務端口 port
            setAttribute("key",  "value") // 其他 TXT, key=value
        } 
        var running: Boolean = true
        _nsdManager.registerService(
              nsdInfo,
              NsdManager.PROTOCOL_DNS_SD,
              object: NsdManager.RegistrationListener {
                    override fun onRegistrationFailed(info: NsdServiceInfo, err: Int) {
                         running = false
                     }
                    override fun onUnregistrationFailed(info: NsdServiceInfo, err: Int) {
                         running = false
                    }
                    override fun onServiceUnregistered(info: NsdServiceInfo) {
                         running = false
                    }
                    override fun onServiceRegistered(info: NsdServiceInfo) {
                         nsdInfo.setServiceName(info.getServiceName())//正式註冊的網路名稱
                         running = false
                    }
              }
        )
        while (running)  { delay(100) }// 暫停 suspend, 100mS 後再回來看是否已完成
        return "$nsdInfo" // 結束後回傳字串
    }

要注意上述 servicePort 是公告所要服務的端口, 與 NSD 本身並不相關, NSD 使用的是群組廣播 multicast 通道, 可以公告多種服務類型, 公告服務單純只用來註冊網路服務, 當公告的名稱一有砥觸,Android 自動會在名稱後面添加一些文字以茲區別,探索服務則是針對服務類型主動發送請求,用以解析對方 ip 與 port, 因此等待時間可能會久一點, 程式邏輯當然也能用上述定時輪詢(pooling)的方式查看旗標狀況來實現, 但這次不使用 suspend fun 函式, 改用一般普通的函式先呼叫 runBlocking 讓裡面的 supspend fun 凍結在該區塊內, 同時藉由 launch 發動 coroutine 所得到的任務名稱 job, 作為將來解析完成後自動把 coroutine 解離, 並加以跳脫 runBlocking 區塊, 因為 runBlocking 會等所有 coroutines join 完成就自動解職離開, 當要探尋多種服務管道時, 這樣的程式運作邏輯, 或許較有效率, 詳細程式碼如下:
    import java.net.InetAddress
    data class IP_PORT(val ip: InetAddress?, val port: Int){ }
    private fun nsdDiscorverHost(
        sniffName: String="name2Find",
        sniffType: String="_http._tcp"
     ) : IP_PORT  {
        if (sniffType.length == 0 || sniffName.length == 0) return IP_PORT(null, 0)
        var sniffHost: InetAddress? = null
        var sniffPort: Int = 0
        lateinit var job: Job
        val nsdLinstener = object: NsdManager.DiscoveryListener {
            override fun onDiscoveryStarted(msg: String) { } 
            override fun onServiceLost(info: NsdServiceInfo) { }
            override fun onDiscoveryStopped(msg: String) {
                if (! job.isCompleted)  job.cancel( )
            }
            override fun onStopDiscoveryFailed(msg: String, err: Int) {
                _nsdManager.stopServiceDiscovery(this)
            }
            override fun onStartDiscoveryFailed(msg: String, err: Int) {
                _nsdManager.stopServiceDiscovery(this)
            }
            override fun onServiceFound(info: NsdServiceInfo) {
                val discovery = this // this is a DiscoveryListener
                if (info.serviceName.contains(sniffName)) {
                      _nsdManager.resolveService(
                           info,
                           object: NsdManager.ResolveListener {
                                  override fun onResolveFailed(resolv: NsdServiceInfo, err: Int) { }
                                  override fun onServiceResolved(resolv: NsdServiceInfo) {
                                       sniffHost = resolv.host
                                       sniffPort = resolv.port
                                       // if (resolv.serviceName == sniffName)
                                       _nsdManager.stopServiceDiscovery(discovery)
                                  }
                          }
                      )
                }
                // ...
            }
        }
        runBlocking { // 最多凍結 10 秒
            job = launch { // get job, launch 無傳回值:  ( )-> void
                  _nsdManager.discoverServices(
                      sniffType,
                      NsdManager.PROTOCOL_DNS_SD,
                      nsdLinstener
                  ) // 呼叫的一般函式並不會被凍結, 裏面持續在運行
                  delay(10000) // 呼叫 suspend 函式時, 本身暫時凍結 10 秒
            } // 一旦 job 被 cancel, 裏面所屬父子代程序也將一併消失
        }
        return IP_PORT(sniffHost, sniffPort)       
    }

主程式如要註冊及探尋網路服務可以用 GlobalScope.launch 同時運行上述兩個 coroutines:
    GlobalScope.launch(Dispatchers.Main) {
        launch {
            val msg  = async {// async 有傳回值: Deferred<String>
                    nsdRegisterTCP("http",  8080) // 這是 async 最後一行函數值
            }
            print("${msg.await( )}")// method Deferred.await( ) to get future value
        }
        launch {
            val find = async {{// async 有傳回值: Deferred<String>
                      nsdDiscorverHost("MyPhone",  "_http._tcp") // 這是 async 最後一行函數值
            }
            print("${find.await( )}")// method Deferred.await( ) to get future value
        }
    }

上面用的  runBlocking, GlobalScope.launch 區塊是在函式內用來發動 coroutine 的工具, 也就是說他是介於一般 routine 與 coroutine 之間運作的橋樑, 當然放在 suspend fun 也是可以的, runBlocking 會凍結函式的運作, 但 GlobalScope.launch 不會, 至於裏面的 launch 及 async 兩種區塊, 他們之間的差異點是: launch 後的 job 不會傳回值, 但 async 區塊可以傳回區塊裏面最後一行的函數值, 發動之後要用方法 await( ) 等待 async 內所有 coroutine 完成後就能取得函數值, 類似 Javascript async/await 的用法, 當 coroutines 放在不同的 launch 區塊時, 他們是獨立並行運作的, 無法斷定何者會先完成, 至於區塊裡面的 coroutines 卻是循序運作的, 而且 runBlocking, GlobalScope.launch, launch , async 等區塊內並不侷限用 suspend fun, 一般方式寫的同步函式也可以放在裏面, 只是它無法被暫停或重置.

最後要再次強調的是 suspend fun 只能由同樣是 suspend fun 類型的函式去呼叫, 或是交由 Kotlin CoroutineScope builder  像是 runBlocking 或 GlobalScope.launch 讓它在區塊範圍內運作

2020年8月12日 星期三

linux 上用 iptables 阻止程式上網

在 Linux mint , 預設使用者是 mint, 可以切換成 su, 用  groupadd 建立一個新群組(例如 wine), 後續就能用 usermod -aG 添加使用者
#!/bin/bash
wineuser=mint
egrep -qs "^wine:" /etc/group || sudo groupadd wine
egrep -qs "^wine:.*:$wineuser" /etc/group ||  usermod -aG wine $wineuser

使用 iptables 的匹配選項 owner 可以針對群組開創一條新規則, 將該群組的使用者與網路斷然隔絕:sudo  iptables  -A  OUTPUT  -m  owner  --gid-owner  wine  -j  REJECT

 要注意的是 group 增添新成員,  必須重新開機才會生效, 因此若寫成 script 執行, 必須先執行一次再重開機,若將上述指令一同放在 /etc/rc.local 裏面時,至少要重新開機兩次才能用 sg 切換群組!

未來開啟終端機, 要執行程式時, 只要下命令前加上 sg   wine  先切換成新群組 wine 後才執行程式, 就可以限制該程式上網的能力,例如寫程式常用的 vscode, 執行命令是 code, 用以下命令, 就能防止它無端傳送封包:
       sg    wine    code
若想要暫時解禁, 可以在 vscode 裏面先開啟終端機, 接著切換成 mint 原始群組, 讓終端機未來所要執行的程式恢復使用網路功能:
       sg  mint
       ...
運作到最後,在終端機下達離開指令
       exit
終端機就會再度禁止網路運作

2020年8月7日 星期五

關於 RC 低通濾波器

一個電阻串接一個下地電容就可以當低通濾波器, 如下所示:
                    Vi     ---> R     +  --- > Vo
                                            C
                                         GND
                   電容阻抗 = 1/sC
                   Vo = Vi * ( 1/sC) ÷ ( R + 1/sC) = Vi  / (1 + sRC)
                   Vo÷Vi =  1 / (1 + sRC)
                    s =  jω
                   Gain = | Vo/Vi|  = 1 / (1+ (ωRC)^2)
- 3 db 頻率點是當輸出是原輸入的"根號 2分之一" =  1/√ 2 時:
                   20log( 1/2) = - 3 = 20log(Gain)
                  Gain = 1/2
                  1 +    RC)^2  = 2 => ωRC = 1
                  ω = 2πf
                  f = 1/2πRC
也就是說將時間常數 RC 乘上 2π 之後, 再取倒數就是該頻率點, 將該頻率以 1V 輸入至此低通濾波器輸出就會得到  0.707 V, 也就是說會衰減 3db
以通式 20 log(Gain) 來看:
                  20log[1 / (1+ (ωRC)^2)] = - 10log[ 1 + (ωRC)^2]
                  當 (ωRC)^2 >> 1 時
                  20 log(Gain) = - 10log[ 1 + (ωRC)^2] ~= - 20log[ ωRC]
意思是說若以 f = 1/2πRC 當作基準頻率的 -3db 點(信號強度剩 1/√ 2), 則未來該基準頻率 10 倍頻的地方將會衰減 20 db(信號強度剩 1/10), 而 100 倍頻時就會衰減 40 db(信號強度剩 1/100), 而 1000 倍頻時就會衰減至 60 db(信號強度剩 1/1000), ... 以此類推, 換句話說就是每 10 倍頻以斜率  20db 在線性衰減

使用 OP LM324 當作緩衝器注意事項

參考文章: https://www.analog.com/en/analog-dialogue/articles/avoiding-op-amp-instability-problems.html#

LM324 是內含 4 個 OP 的運算放大器, 參考技術資料 : https://www.onsemi.com/pub/Collateral/LM324-D.PDF, 工作電壓是 3V ~ 32V , 可以單電壓工作, 但有些事項需要特別注意:

1. 輸出端 Vo 線性範圍,  供應電壓 Vcc (正端電源), Vee (負端電源), 與 Vb 偏壓(bias voltage)

從 datasheet 可看出 LM324 是 bipolar 輸出, 當偏壓 Vb = (Vcc - Vee) ÷ 2 時, 負輸出(Vo < Vb) ,可以接近 Vee, 但正輸出(Vo > Vb) 受限電晶體的特性, 最大輸出電壓將會比正端電源 Vcc 稍低個 1 ~ 1.3, 因此若想用電壓 3.3V 當電源工作時, 會看到最大輸出到接近 2V 時就飽和被截掉了, 輸出若是 Vo < Vee 時,當然也會被截掉變成 Vee

2. 放大器輸入電阻 Ri, 回受電阻 Rf,  信號源 Vs, 信號源電阻 Rs

若是操作運算放大器在反相放大模式, 當回受電阻等於 Rf, 輸入電阻等於 Ri, 放大率就是 - Rf ÷Ri, 也就是說可以將信號放大或縮小, 端視 Rf 與 Ri 的比率而定, 而且輸出信號相位與原輸入信號還差了 180 度:

         Vo = - Vi  * Rf ÷ Ri   , 反相放大

若是操作在非反相放大模式, 放大率則變成 1 + Rf ÷Ri, 永遠大於 1, 換句話說只能把信號放大:

        Vo =  Vi  * (1 + Rf ÷ Ri) = Vi *(Ri + Rf) ÷ Ri   , 非反相放大

當信號源 Vs 的輸出阻抗 Rs 遠小於 Ri, 就可以忽略掉信號衰減效應 Vi = Vs, 否則就要考慮進去:

         Vi = Vs * Ri ÷ (Ri + Rs)

因此, 反相放大器輸出:

        Vo = -  Vs * [Ri  ÷ (Ri + Rs)]  *  Rf ÷ Ri 

              = -  Vs * Rf ÷ (Ri + Rs) 

非反相放大輸出 :
        Vo  =  Vs * [Ri  ÷ (Ri + Rs)] * [(Ri + Rf) ÷ Ri]

               =  Vs * (Ri + Rf )÷ (Ri + Rs)

雖然可以透過 Rs 把數值加大將信號衰減, 但同時也把熱雜訊引入, 因此 Rs 要儘量小, 參考文章: https://www.analog.com/en/analog-dialogue/raqs/raq-issue-25.html

2020年8月6日 星期四

使用 Javascript 用 webGL 畫線

參考資料:   https://www.tutorialspoint.com/webgl/index.htm             
<html><head><meta charset='utf-8'>    
    <script>
        class PixelRGBA { // float32[4] RGBA, 1 pixel color
            constructor(r, g, b, α = 1) { this.color = new Float32Array([r, g, b, α]);  }
        }
        class ColorLine extends PixelRGBA {// default α = 1
            constructor(nXnY = 512, r = 8, g = 0, b = 0, isSolid = true, aspect = 1.0) {
                super(r, g, b);
                this.pixels = nXnY;// , nX = nY = nXnY, number of X, Y
                this.isSolid= isSolid;
                this.visible = true;
                this.lineID = 0;// ID allocated by ScopeWebGL addLine()
                this.eXoY = new Float32Array(nXnY << 1);// even X, odd Y for (x, y)
                this.viewStart = 0;
                this.η = 0;// 0 <= η < this.pixels
                // keep const λ = 2 = N * Δx, so that x begin with -1 to be in range [-1, 1]
                const λ  = 2.0;// x range [-1, 1], max length = 2, to be a float number
                const N  = isSolid ? nXnY : nXnY >> 3;// line pixels,to be a interger number
                const Δx = λ / N;// x interval, will be a float number
                const Δy = aspect * Δx;// y dependent on x for vertical/horizontal line
                for (let i = 0, n = 0, x = -1.0, y = -aspect; i < N; i ++, n += 2) {
                    this.eXoY[n] = x; // even: x coordinate [-1, 1], Δx = 2 ÷ nXnY
                    this.eXoY[n + 1] = y;// odd: ycoordinate [-1, 1],Δy = aspect * Δx
                    x += Δx;
                    y += Δy;
                }// line.eXoY is Float32Array, 4 bytes/float, 8 bytes per (x,y)

                this.vertical = (x) => { // set vertical line @x
                    for (let i = 0, n = 0; i < this.pixels; i ++, n += 2) {
                        this.eXoY[n] = x;// todo: validate
                    }
                    return this;// to be chain this member
                }
                this.horizon = (y) => { // set horizon line@y
                    for (let i = 0, n = 1; i < this.pixels; i ++, n += 2) {
                        this.eXoY[n] = y;// todo: validate
                    }
                    return this;// to be chain this member
                }
                this.injectFrame = (data) => { // to inject a frame of data at tail
                    if (data && (data.length > 0)) {
                        let moveEnd = this.pixels - data.length;
                        if (moveEnd < 0) moveEnd = 0;
                        const start = data.length * 2;
                        for (let i = 0, n = 1; i < moveEnd; i ++, n += 2) {
                            this.eXoY[n] = this.eXoY[start + n];// only copy y at odd
                        }
                        for (let i = 0, n = (moveEnd * 2) + 1; i < data.length; i ++, n += 2){
                            this.eXoY[n] = data[i];// only copy y at odd
                        }
                    }
                    return this;// to be chain this member
                }
            }            
            // amplitude(n, y) { this.eXoY[n * 2 + 1] = y;  }
            get y() {
                let n = this.η + this.viewStart ;
                if (n >= this.pixels) n = this.pixels - 1;
                return this.eXoY[(n << 1) + 1];
            } // read only
            /*set y(tempy) {// write only
                let n = this.η + this.viewStart ;
                if (0 > n || n >= this.pixels) return;                
                this.eXoY[(n << 1) + 1] = tempy;
            }*/
            
            get position() { return this.η - (this.pixels >> 1); } // read only, refer to center point
            set position(n){ // to set position @n sample, 0<= n < pixels
                if(n >= this.pixels) n = this.pixels - 1;// saturation
                else if(n < 0) n = 0;                
                this.η = n;// 0<= η < pixels
            } // write only

            get x() { return this.eXoY[this.η << 1];  }// read only
            set x(tempx){// to set position @x [-1.0<= x <=1.0] mapto [0 - pixels]
                let n = Math.floor((tempx + 1) * this.pixels) >> 1;// level shift
                this.position = n;
            } // write only
        }

        class ScopeLineGL {// https://www.tutorialspoint.com/webgl/index.htm              
            constructor() {
                const gpuVertexSource =`
                    uniform mat2 scaling;
                    attribute vec2 coordinates;
                    uniform vec2 translation;
                    void main(void) {
                        gl_Position = vec4(scaling * coordinates + translation, 0, 1.0);
                    }
                `;
                const gpuShaderSource =`
                    precision mediump  float;
                    uniform highp vec4 color;
                    void main(void) {
                        gl_FragColor = color;
                    }
                `;
                //const filterGain  = document.createElement('span');// to output text
                const yscaleInput = document.createElement('input');// to input gain value    
                const xcursorInput= document.createElement('input');// to input level trigger value
                const ycursorInput= document.createElement('input');// to input level trigger value   
                const xscaleInput = document.createElement('input');// to input level trigger value   
                const spanBreak   = document.createElement('br');   // spanBreak.cloneNode(true));
                const divScope    = document.createElement('div');// to group following elements
                const canvas      = document.createElement('canvas');// to display oscilloscope
                const coordinatex = document.getElementById('coordinatex');
                const edgeToggle  = document.getElementById('edgeToggle');
                const xcursorToggle= document.getElementById('xcursorToggle');
                const ycursorToggle= document.getElementById('ycursorToggle');
                this.resetToDefault= document.getElementById('resetToDefault');
               
                document.body.style='background-color:black;';
                document.body.append(yscaleInput);
                document.body.append(spanBreak);
                document.body.append(xcursorInput);
                document.body.append(divScope);
                divScope.append(ycursorInput);
                divScope.append(spanBreak.cloneNode());
                divScope.append(canvas);
                divScope.append(spanBreak.cloneNode());
                divScope.append(xscaleInput);
                
                const pixelRatio = window.devicePixelRatio || 1;
                this.width = 2048;
                this.height= 256;
                this.aspectratio = this.width / this.height;
                canvas.width = Math.round(this.width * pixelRatio);// pixels@foreground, to fix center point?
                canvas.height= Math.round(this.height* pixelRatio);// canvas.getContext('2d');

                const webgl  = canvas.getContext('webgl', {antialias: true, transparent: false});//'experimental-webgl'
                webgl.enable(webgl.DEPTH_TEST);
                webgl.clear(webgl.COLOR_BUFFER_BIT || webgl.DEPTH_BUFFER_BIT);
                webgl.viewport(0, 0, canvas.width, canvas.height);
                const gpuProgram = webgl.createProgram();
                const vertexCode = webgl.createShader(webgl.VERTEX_SHADER);            
                const fragmentCode = webgl.createShader(webgl.FRAGMENT_SHADER);
                webgl.shaderSource(vertexCode, gpuVertexSource);
                webgl.shaderSource(fragmentCode, gpuShaderSource);
                webgl.compileShader(vertexCode);
                webgl.compileShader(fragmentCode);
                webgl.attachShader(gpuProgram, vertexCode);
                webgl.attachShader(gpuProgram, fragmentCode);
                webgl.linkProgram(gpuProgram);
                this.clear = () => webgl.clear(webgl.COLOR_BUFFER_BIT || webgl.DEPTH_BUFFER_BIT);
            
                this.lines = [];// to store all lines, todo: use map { } , not array []
                this.lineID = 0;// serial number to be allocated
                this.clientWidth = canvas.width; // scaled by drag
                this.clientHeight = canvas.height;// scaled by drag
                this.disableLine = (id) => { // mark as invisible
                    if(id < this.lineID) this.lines[id].visible = false;
                    return this;
                }
                this.addLine = (line) => {
                    line.lineID = this.lineID ++;
                    this.lines.push(line);
                    return line.lineID;
                }
                
                this.addLine(new ColorLine(this.width, 10, 10, 10).horizon(0));// solid grey horizontal line at x=0
                this.addLine(new ColorLine(this.height, 10, 10, 10, true, this.aspectratio).vertical(0));// solid grey vertical line at y=0
                this.triggerLineID1 = this.addLine(new ColorLine(this.width, 10, 0, 0, false));// dash red trigger line;
                this.triggerLineID2 = this.addLine(new ColorLine(this.width, 10, 0, 0, false));// dash red trigger line;
                this.timeLineID1 = this.addLine(new ColorLine(this.height, 0, 10, 0, true, this.aspectratio));// solid green time line;   
                this.timeLineID2 = this.addLine(new ColorLine(this.height, 0, 10, 0, true, this.aspectratio));// solid green time line;         
                this.scopeLineID = this.addLine(new ColorLine(this.width, 10, 10, 0));// slop = 1, solid yellow line                      
                
                const abs = (v) => v < 0 ? -v : v;// 取絕對值
                const round = (f, d = 2) => {// default 小數 2 位
                    let dot = 10 ** d;
                    return Math.floor(f * dot + 0.5) / dot;
                }
                const tText = (uS) => abs(uS) >= 1e6 ? `${round(uS / 1e6)}sec` :
                                      abs(uS) >= 1e3 ? `${round(uS / 1e3)} ms` :
                                      `${round(uS, 0)} uS`;// 單位四捨五入, 小數點 2 位
                const reportCoordinate = () => {
                    const scopeLine = this.lines[this.scopeLineID];
                    const  fs = this.sampleRate;
                    const  Δx = abs(this.lines[this.timeLineID1].eXoY[0] - this.lines[this.timeLineID2].eXoY[0]);
                    const  Δy = abs(this.lines[this.triggerLineID1].eXoY[1] - this.lines[this.triggerLineID2].eXoY[1]);
                    const tΔx = round(scopeLine.pixels * Δx * 5e5 / fs, 0); // uS, [-1, 1] ↕↔
                    const ΔHz = (tΔx == 0) ? 'Hz' : `${round(1e6 / tΔx, 0)} Hz`;
                    const   n = scopeLine.position;// refer to O, may be a negative number
                    const nΔt = n * 1e6 / fs;// uS unit
                    let y = round(scopeLine.y);
                    if (y > 0) y = `+${y}`;
                    coordinatey.innerHTML =`<span>↕@y= ${round(this.triggerLevel)} v, Δy= ${round(Δy)} v</span>`;
                    coordinatex.innerHTML =`<span'>↔@fs= ${fs} Hz, y= ${y} v<br>n= ${n}, t= ${tText(nΔt)}<br>Δt= ${ΔHz}<sup>-1</sup> = ${tText(tΔx)}</span>`;
                }
                
                const scaleTable  = [16, 8, 4, 2, 1, 1.0/2, 1.0/4, 1.0/8, 1.0/16];
                yscaleInput.type  = 'range';   
                yscaleInput.min   = 0;   
                yscaleInput.max   = scaleTable.length - 1;
                xscaleInput.type  = 'range';// rotate 180 degreen
                xscaleInput.min   = 0;
                xscaleInput.max   = scaleTable.length - 1;
                ycursorInput.type = 'range';// rotate 90 degreen  
                xcursorInput.type = 'range';   
                
            // to calculate global offset
                const ox0 = yscaleInput.clientWidth;// pixels
                const oy0 = ycursorInput.clientHeight;// pixels
                const oTranslate = `translate(0, ${oy0 - ox0}px)`;
                const rotate90deg= 'transform-origin:0 100%; transform: rotate(90deg)';//clockwise
                const rotate180deg='transform-origin:50% 50%; transform: rotate(180deg)';               
                xcursorInput.style= `width:${this.clientWidth}; ${rotate180deg} translate(${-ox0}px, 0)`;
                ycursorInput.style= `width:${this.clientHeight}; ${rotate90deg} ${oTranslate};`;
                yscaleInput.style= rotate90deg;
                xscaleInput.style= rotate180deg;
                const xoffset = ox0 * 2 / this.clientWidth; // Δx = 2.0 ÷ nX;
                const yoffset = 0;
                this.xcursorID = false;
                this.ycursorID = false;
                canvas.style.border= '1px solid';// 外框 1 點
                // canvas.style= `transform-origin:0% 0%; transform: translate(${ox0}px, 0)`;
                
                webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
                webgl.useProgram(gpuProgram);
                const gpuColor  = webgl.getUniformLocation(gpuProgram, 'color');  
                const gpuScaling = webgl.getUniformLocation(gpuProgram,'scaling');
                const coordinates = webgl.getAttribLocation(gpuProgram,'coordinates');
                const gpuTranslate= webgl.getUniformLocation(gpuProgram,'translation');
                webgl.uniform2fv(gpuTranslate, new Float32Array([xoffset, yoffset]));
                webgl.vertexAttribPointer(coordinates, 2, webgl.FLOAT, false, 0, 0);
                webgl.enableVertexAttribArray(coordinates);
                
                const xyDraw = (eXoY, pixels, color = PixelRGBA(10,10,10), isSolid = true) => {
                    if (line) { // lambda can bind this & webgl to useProgram ;// webgl.useProgram(gpuProgram);
                        webgl.uniform4fv(gpuColor, color);// transfer color to GPU
                        webgl.uniformMatrix2fv(gpuScaling, false, new Float32Array([
                            this.xscale, 0,
                            0, this.yscale
                        ])); // transfer a matrix for global scaler to GPU
                        webgl.bufferData(webgl.ARRAY_BUFFER, eXoY, webgl.STREAM_DRAW);// stream the position (x,y)
                        webgl.drawArrays(isSolid ? webgl.LINE_STRIP : webgl.LINES, 0, pixels);// line render
                        // webgl.drawArrays(isSolid ? webgl.POINTS : webgl.LINES, 0, viewPixels);
                    }
                }                
                this.draw = (line, eXoY = line.eXoY, pixels = line.pixels) =>
                    xyDraw(eXoY, pixels, line.color, line.isSolid);

                edgeToggle.onclick = () => {
                    this.positiveTrigger = ! this.positiveTrigger;// toggle
                    edgeToggle.innerHTML = `${this.positiveTrigger ? '↑正緣':'↓負緣'}觸發`;
                }                            
            // cursor y to capture position as triggerLevel and show horizontal line
                ycursorInput.oninput  = () => { // direction inverse
                    // console.log(ycursorInput.value);
                    const temp = - ycursorInput.value * 2 / this.height;// mirror and scale down to [-1, 1]
                    this.triggerLevel = temp;
                    this.lines[this.ycursorID ? this.triggerLineID1 : this.triggerLineID2].horizon(temp);
                    reportCoordinate();// todo: report y only
                }
            // cursor x to capture position as time@x and show vertical line
                xcursorInput.oninput = () => { // direction inverse
                    // console.log(xcursorInput.value);
                    const temp = - xcursorInput.value * 2 / this.width;// mirror and scale down to [-1, 1]
                    this.lines[this.scopeLineID].x = temp;
                    this.lines[this.xcursorID ? this.timeLineID1 : this.timeLineID2].vertical(temp);
                    reportCoordinate();// todo: report x only
                }
            // toggle y cursor ID  
                ycursorToggle.onclick = () => {
                    this.ycursorID = ! this.ycursorID;// toggle
                    ycursorInput.oninput();// to sync with y-cursor
                }
            // toggle xcursor ID
                xcursorToggle.onclick = () => {
                    this.xcursorID = ! this.xcursorID;// toggle
                    xcursorInput.oninput();// to sync with x-cursor
                }
            // amplitude scale
                yscaleInput.oninput  = () => {// inverse by scaleTable
                    this.yscale = scaleTable[yscaleInput.value];
                    const temp = 1.0 / this.yscale;// to syn with _gpuYscale
                    ycursorInput.step = temp;
                    ycursorInput.min = -temp * this.height / 2;// Δy = 2/this.height
                    ycursorInput.max = temp * this.height / 2;
                    ycursorInput.oninput();// to sync with y-cursor
                };
            // time scale
                xscaleInput.oninput  = () => {// inverse by scaleTable
                    this.xscale = scaleTable[xscaleInput.value];
                    const temp = 1.0 / this.xscale;// to syn with _gpuXscale
                    xcursorInput.step = temp;
                    xcursorInput.min = -temp * this.width / 2;// Δx = 2/this.width
                    xcursorInput.max = temp * this.width / 2;
                    xcursorInput.oninput(); // to sync with x-cursor
                }
                this.resetToDefault.onclick = () => {
                    yscaleInput.value = 5; // 0.5X => use table to invese direction
                    xscaleInput.value = 4; // 1X  => use table to invese direction
                    ycursorInput.value= 0;// +:down, 0:original@center, -:up
                    xcursorInput.value= 0;// 1:left, 0:original@center, -1:right
                    this.positiveTrigger = false;
                    this.triggerLevel = 0.0;
                    this.lines[this.triggerLineID1].horizon(0);
                    this.lines[this.triggerLineID2].horizon(0);
                    this.lines[this.timeLineID1].vertical(0);
                    this.lines[this.timeLineID2].vertical(0);  
                    this.lines[this.scopeLineID].viewStart = 0;                
                    edgeToggle.onclick();
                    yscaleInput.oninput();
                    xscaleInput.oninput();
                }   
                this.xcursorInput = xcursorInput;// to update cursor infomation realtime
                this.resetToDefault.onclick();
            }// end of constructor
            get render () { // getter can bind this
                this.lines.forEach( (line) => { if (line.visible) {
                    if(line.lineID == this.scopeLineID) { // scopeLine need to be trigger
                        const level = this.triggerLevel;
                        const positive = this.positiveTrigger;
                        const halfPoints = line.pixels >> 1; // begin from center point
                        let n = halfPoints * 2 + 1;// index of center y
                        let py = line.eXoY[n];// store first point
                        n += 2; // go ahead next point, to skip x coordinate
                        for(let i = 1; i < halfPoints; i ++, n += 2) {
                          let y = line.eXoY[n];// trigger seraching            
                          if (positive && y > py &&
                                py < level && level <= y) {
                                line.viewStart = i; // positive trigger
                                break;
                          } else if (! positive && y < py &&
                                py > level && level >= y) {
                                line.viewStart = i; // negative trigger
                                break;
                          }
                          py = y; // keep previously value to check edge
                        }
                    }
                    if(line.viewStart == 0) this.draw(line);                       
                    else {
                        const eXoY = new Float32Array(line.eXoY);//to keep same x coordinate
                        const pixels = line.pixels - line.viewStart;// pixels to move
                        const indexShift= line.viewStart << 1; // index shift
                        let n = 1;// to update y coordinate only, it begins at 1
                        for(let i = 0; i < pixels; i ++, n += 2) {
                            eXoY[n] = line.eXoY[n + indexShift];
                        }    
                        this.draw(line, eXoY, pixels);// pixels has been shrink
                        // for(let i = pixels; i < line.pixels; i ++, n += 2) eXoY[n] = 0;
                        // this.draw(line, eXoY);            
                    }
                }});
            }
            set triggerLevel(level) {
                if(-1<= level && level <=1) this._triggerLevel = level;  
            }
            get triggerLevel() { return this._triggerLevel || 0.0; } // to prevent Null Pointer Exception

            get sampleRate() { return this._sampleRate || 1; } // to prevent Null Pointer Exception
            set sampleRate (fs) {// fs per second
                if(fs >= 1) {
                    this._sampleRate = fs;
                    this.resetToDefault.onclick();
                    this.render;
                }
            }
        }
    </script>
</head>
<body>
    <button id='resetToDefault' style="float: left;">重設 Reset</button>
    <button id='ycursorToggle' style="float: right; color: red">標記相對振幅 y 起始點</button>
    <div id='coordinatey' style='font-size: 48px; text-align:left; color:red'>coordinatey</div>
    <button id='xcursorToggle' style="float: right; color:green">標記相對時間 t 起始點</button>  
    <button id='edgeToggle' style="float: left;">↑ 邊緣觸發</button>
    <div id='coordinatex' style='font-size: 48px; text-align:left; color:green'>coordinatex</div>
    <script>
        const oscScope= new ScopeLineGL();
        const line = oscScope.lines[oscScope.scopeLineID];
        
        const data = new Float32Array(128);
        const randomSingal = () => {
            for(let i = 0; i < data.length ; i ++) {
                data[i] = 2 * Math.random() - 1;
            }
            line.injectFrame(data);
            oscScope.render;
            requestAnimationFrame(randomSingal);     
        }
        randomSingal();
    </script>
</body>
</html>

2020年7月2日 星期四

用 ESP8266 驅動 BLDC

之前硬碟故障後一直保存在庫房裏, 裏面有一棵 3 相 BLDC 包在鋁合金裏面, 無法拆, 索性上網買兩顆橋式電路板練習用 PWM(pulse width modulation)來驅動看看, 3 相 BLDC 端點電壓 A, B,  C 在相對應的 U, V, W 相位差需彼此相差 120 度才能驅動, 同一時間點 BLDC 的其中兩端點根據電流流向產生磁極去吸引馬達轉子(永久磁鐵)達到正轉或逆轉的目的, 第 3 端點必須保持高阻抗讓它自由滑行, BLDC 雖中文翻譯為無刷直流馬達, 實際上卻是一個名不符實的交流馬達, 只是他不用負電壓去驅動, 而是靠端點相對電壓產生負電流去驅動, 透過 PWM 信號控制橋式電路板就能產生交流(正負)電流, 網上買的橋式 IC 是 MX1508, 他有兩個全橋式通道, 每個通道是 2 個輸入, 去控制 2 個橋式輸出,真值表如下:
         In+    In-     Out+    Out-
           0       0       HiZ       HiZ
           0       1       GND     VCC
           1       0       VCC      GND
           1       1       煞車      煞車

驅動 3 個相位的 BLDC 需要用 2 棵 MX1508 IC 將直流 PWM 信號轉換成交流的橋式電流去推動馬達的電感性負載, 2 棵 MX1508 就有 4 個全橋式通道可以使用, 只要取其中 3 個通道, 另外一個通道的兩個輸入端直接接地避免誤動作,  順轉時,電流推導順序如下:
    step 1.   A -> B (C HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 1), C+ = 0, (C- = 0)
    step 2.   A -> C (B HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 0), C+ = 0, (C- = 1)
    step 3.   B -> C (A HiZ):  A+ = 0, (A- = 0) , B+ = 1, (B- = 0), C+ = 0, (C- = 1)
    step 4.   B -> A (C HiZ):  A+ = 0 , (A- = 1), B+ = 1, (B- = 0), C+ = 0, (C- = 0)
    step 5.   C -> A (B HiZ):  A+ = 0 , (A- = 1), B+ = 0, (B- = 0), C+ = 1, (C- = 0)
    step 6.   C -> B (A HiZ):  A+ = 0 , (A- = 0), B+ = 0, (B- = 1), C+ = 1, (C- = 0)

如果要逆轉, 只要將順序反過來就可:
    step 1.   C -> B (A HiZ):  A+ = 0 , (A- = 0), B+ = 0, (B- = 1), C+ = 1, (C- = 0)
    step 2.   C -> A (B HiZ):  A+ = 0 , (A- = 1), B+ = 0, (B- = 0), C+ = 1, (C- = 0)
    step 3.   B -> A (C HiZ):  A+ = 0 , (A- = 1), B+ = 1, (B- = 0), C+ = 0, (C- = 0)
    step 4.   B -> C (A HiZ):  A+ = 0, (A- = 0) , B+ = 1, (B- = 0), C+ = 0, (C- = 1)
    step 5.   A -> C (B HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 0), C+ = 0, (C- = 1)
    step 6.   A -> B (C HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 1), C+ = 0, (C- = 0)

之後循環重複  1 ~ 6 步驟就能產生 120 度相位差, 持續推動 BLDC, 底下只看順轉的情形, 搭配上述真值表, 把電流的流向控制邏輯數位化成一個 6x6 矩陣表來表示, 橫軸是時間軸, 縱軸是每個數位化控制邏輯:

  const int motorPhases[6][6] = {
     {1, 1, 0, 0, 0, 0}, // A+: 1, 1, z, 0, 0, z
     {0, 0, 0, 1, 1, 0}, // A- : 0, 0, z, 1, 1, z
     {0, 0, 1, 1, 0, 0}, // B+: 0, z, 1, 1, z, 0
     {1, 0, 0, 0, 0, 1}, // B- : 1, z, 0, 0, z, 1
     {0, 0, 0, 0, 1, 1}, // C+: z, 0, 0, z, 1, 1
     {0, 1, 1, 0, 0, 0}  // C- : z, 1, 1, z, 0, 0
  };
  mx1508 真值表:
  輸入(+-)    輸出(+-)
     00       -> GND, GND : 高阻抗 HiZ
     10       ->  VCC, GND : 電流順向 Forward
     01       ->  GND, VCC : 電流反向 Reverse
     11       ->  VCC, VCC  : 煞車 Brake

另外要特別注意的是 MX1508 耐電壓只到 9V, 驅動力有限, 可能無法產生太大扭力, 尤其使用 PWM 訊號去驅動時, 有效扭力與 duty cycle 成正比, 因此可能還要打個折. 想要讓 BLDC 保持高速運動, 就算沒有 sensor 也可以推動, 就要像盪鞦韆一樣慢慢的定時加速. 下面要利用 ESP8266 的 timer1 中斷服務程式(interrupt service routine) 從 GPIO 輸出正相位 phase0A, phase0B, phase0C 去控制橋式電路的 U, V, W 相, 也就是說將相對應橋式輸出端分別連接馬達的 U, V, W 相, 至於負相位 phase_A, phase_B, phase_C 主要是控制MX1508 的輸出邏輯, 因此負相位要與橋式輸入相對應的相位連接, 但橋式輸出端就空著不用接,換句話說連接 BLDC 的其實只用通道的半橋式輸出電路, 而控制時運用全橋式輸入邏輯. 相位順序必須連接正確否則是無法推動 BLDC 的,完整 arduino 程式碼如下:
// app.ino
#include "ESP8266WiFi.h"
#define phase0A   14 // A+ use GPIO14: D5
#define phase_A   12 // A- use GPIO12 : D6
#define phase0B   13 // B+ use GPIO13 : D7
#define phase_B   15 // B-  use GPIO15 : D8
#define phase0C   4  //  C+ use GPIO4 : D2
#define phase_C   5  //  C- use GPIO5 : D1
#define RED_LED  2  // GPIO2  is D4
const int motorPhases[6][6] = {
  {1,1,0,0,0,0},// A+: 1, 1, z, 0, 0, z
  {0,0,0,1,1,0},// A- : 0, 0, z, 1, 1, z
  {0,0,1,1,0,0},// B+: 0, z, 1, 1, z, 0
  {1,0,0,0,0,1},// B- : 1, z, 0, 0, z, 1
  {0,0,0,0,1,1},// C+: z, 0, 0, z, 1, 1
  {0,1,1,0,0,0} // C- : z, 1, 1, z, 0, 0
};
int currentPhase = 0;
uint32_t prescaler = 5e6;
int downCounter = 5e6/prescaler;// counter upto 1 second

void ICACHE_RAM_ATTR  onTimeCounter( ) {
  timer1_write(prescaler);
  if(prescaler < 2e6) {     
    digitalWrite(RED_LED, 1);// Turn off LED, begin to speed up ...
    digitalWrite(phase0A , motorPhases[0][currentPhase]);
    digitalWrite(phase_A , motorPhases[1][currentPhase]);
    digitalWrite(phase0B , motorPhases[2][currentPhase]);
    digitalWrite(phase_B , motorPhases[3][currentPhase]);
    digitalWrite(phase0C , motorPhases[4][currentPhase]);
    digitalWrite(phase_C , motorPhases[5][currentPhase]);
    if (++  currentPhase > 5) currentPhase = 0;  
  } else { // to brake BLDC
    digitalWrite(phase0A, 1);
    digitalWrite(phase_A, 1);
    digitalWrite(phase0B, 1);
    digitalWrite(phase_B, 1);
    digitalWrite(phase0C, 1);
    digitalWrite(phase_C, 1);
  }
  if (downCounter > 0) downCounter --;
  else { // time to speed up
    if(prescaler >= 2e5) {
      prescaler = prescaler * 90 / 100; // to speed up 1/0.9    ~= 1.1
    } else if(prescaler >= 2e4) {
      prescaler = prescaler * 95 / 100; // to speed up 1/0.95 ~= 1.05
    } else if (prescaler >= 10e3) {
      prescaler = prescaler * 98 / 100; // to speed up 1/0.98 ~= 1.02
    } else if (prescaler >= 5e3 ) {
      prescaler = prescaler * 99 / 100; // to speed up 1/0.99 ~= 1.01
    } // else keep speed constant, if prescaler smaller than 4500, kernel panic!
    downCounter  = (625e3/prescaler); // counter upto 1/8 second
  } 
}

void setup( ) {
  Serial.begin(115200);
  pinMode(RED_LED, OUTPUT); 
  digitalWrite(RED_LED, 0);// Turn on LED
  pinMode(phase0A, OUTPUT);// A+
  pinMode(phase_A, OUTPUT);// A-
  pinMode(phase0B, OUTPUT);// B+
  pinMode(phase_B, OUTPUT);// B-
  pinMode(phase0C, OUTPUT);// C+
  pinMode(phase_C, OUTPUT);// C-
  timer1_attachInterrupt(onTimeCounter);
  timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
  timer1_write(prescaler);
}

void loop( ) {
}

仔細觀查 6x6 矩陣表會發現一個有趣的現象  A+  與 A- , B+ 與 B- , C+ 與 C- 都相差(超前或是延後) 3 個時間單位(全相是 6 個時間單位, 一圈是 360 度), 稍微運算後會發現正好就是 + 與 - 相位差 180 度, 而 A+ 與 B+  與 C+ 之間也都相互差了 2 個時間單位, 也是因為相位差 120 度的必然結果:
    360 * 3 / 6 = 180
    360 * 2 / 6 = 120
因此只要保存 baseVector= {1, 1, 0, 0, 0, 0} 這一基礎向量, 其它時間的相位用"取模 6 運算"(% 6) 來得到, 因此推動 BLDC 可以簡單用下面的虛擬運算碼來呈現:
const bool  baseVector[6] = {true, true, false, false, false, false};//向量, 共 6 個時間相 0 ~ 5
int n = 0 ;  // 初始相 0, in phase
while (true) {
    digitalWrite(A+,  baseVector[n % 6]);// in  phase  0
    digitalWrite(A- , baseVector[(n - 3) % 6]);// phase lag 3
    digitalWrite(B+, baseVector[(n - 2) % 6]);// phase lag 2
    digitalWrite(B- , baseVector[(n - 5) % 6]);// phase lag 5
    digitalWrite(C+, baseVector[(n - 4) % 6]);// phase lag 4
    digitalWrite(C- , baseVector[(n - 1) % 6]);// phase lag 1
    n = (n + 1) % 6;
    delay(.);
}

另一種演算法是先分別取出每個相, 結束後再準備下個時間單位的向量, 只要把基礎向量旋轉一個時間單位, 同樣可以達到相同目的:
bool  baseVector[6] = {true, true, false, false, false, false};//初始向量, 共 6 個時間相 0 ~ 5
while (true) {
    digitalWrite(A+, baseVector[0]);// phase 0
    digitalWrite(A- , baseVector[3]);// phase -3 % 6 = phase 3
    digitalWrite(B+, baseVector[4]);// phase -2 % 6 = phase 4
    digitalWrite(B- , baseVector[1]);// phase -5 % 6 = phase 1
    digitalWrite(C+, baseVector[2]);// phase -4 % 6 = phase 2
    digitalWrite(C- , baseVector[5]);// phase -1 % 6 = phase 5
    bool temp =  baseVector[0];
    for (int  i = 0; i < 5 ; i ++) baseVector[i] = baseVector[i + 1];// 順時移動
    baseVector[5] = temp;
    delay(.);
}

因為沒使用 sensor 去感應磁鐵的位置, 因此很有可能一開始的慣性力矩造成旋轉方向錯誤,或是有阻力導致後續驅動 BLDC 的相位無法與旋轉運動產生有效共振而停止運動, 或許 BLDC 必須用 hall sensor 的助力才能穩定運行!

用 emcc compiler 寫簡單的 openGL 繪圖, 讓瀏覽器也能觀看

在瀏覽開啟 html 檔, 裡面除了可以用 javascript 語言來運行openGL ES, 也能用 wasm 語言來運作 , 透過 emcc 編譯器可以將 c 語言翻譯成 wasm, 安裝方式詳如 Emscripten 官網: https://emscripten.org/...