2021年2月15日 星期一

BLE Dashboard - LS060S2UD01 Sharp Memory LCD

 
有次網路逛街看到了好便宜的6" Sharp Memory LCD,手癢就買來玩玩看。因為這次螢幕解析度和大小都滿大的,所以想說來做兩個東西: 顯示天氣圖還有如上圖的PC儀表板。

首先要決定的是要用哪個MCU跟架構,因為Memory LCD本來就低耗電,設計目標自然是帶電池的無線螢幕,所以挑選的項目自然只剩下有無線功能的MCU。

WiFi or BLE? 老實說我想了一陣子,因為800x600資料不少,我原本是很想要用WiFi因為資料下載比較快,但是ESP32的耗電量控制真的慘,不適合這種一直更新的用途,所以還是用BLE。

先來講這片螢幕 LS060S2UD01: 

6" 800x600的解析度,而和之前的Memory LCD差異最大的在於每個pixel是兩個單色sub-pixel組成,所以每個pixel實際上能組成四種黑/白色,資料可以寫4bit的資料,然後控制兩個單色的sub-pixel你就有4色,也可以讓螢幕幫你做dithering擴展成16色階,不過效果在複雜的圖文底下不佳(aka天氣圖)。

這是單色我老婆

這是16色階dithering的我老婆,其實效果還不錯

這是16色階dithering的天氣圖,文字和線條就很慘了

控制介面則是parallel bus,有16bit和8bit兩種寬度,資料刷新的步驟基本上是:

資料傳到螢幕的Frame Buffer => 讓Frame Buffer寫資料進到Panel => 等資料寫完

其中寫資料可以指定寫入部分的螢幕(類似E-ink的partial update),還可以從frame buffer把資料讀出來(為了支援小容量記憶體的MCU我猜)。然後把資料從frame buffer轉移完之後透過ACK pin通知MCU寫入完畢。另外也可以開Video mode讓LCD自己用30Hz的速度自動轉移Frame buffer到Panel,這時候SYNC pin會有Frame Sync通知MCU。

我為了方便,這邊用的是8bit bus + 單色 + Full Update + 自己下指令轉移buffer。可能之後換成4色。因為800x600單色已經是56K的資料量了,開成4bit=>224K,有點肥。而資料寫入的Bus則是透過把parallel bus安排到GPIO 0 ~ 7 + 直接register set/clr,速度可以拉到10Hz。


我的Driver基本上是為了這個MCU設計的,所以僅供參考: https://github.com/will127534/Adafruit_SHARP_Memory_Display

接下來就是MCU,基本上nRF系列也不錯,但是我這次想來試試 SparkFun Artemis Module,因為這個Module用的不是常見的BLE SoC,用的是Apollo3b from Ambiq,他們家的MCU非常暴力省電。大學修電子學的時候略有耳聞有些IC工作點是在課本沒有教的sub-threshold region,因為不是在飽和區,工作的電流可以壓到非常低,通常用在手錶這種非常低功耗的IC上面。
而Ambiq把它用在MCU上,整個模組我的測試功能全開的狀況下Active power只有1mA,非常狂。

不過缺點也很明顯,因為用的人少,軟體的Support其實不怎麼樣,Sparkfun有Arduino Support,但是它是基於Mbed OS,等於是他們包了Mbed OS bridge + Mbed OS去Support Arduino。老實說如果只有這樣的話那就算了,反正我可以直接寫MCU register,但是好死不死他們包的Mbed OS沒有Sleep Mode,也就是說模組會一直耗掉1mA,這我真的不能忍。只好改用Mbed OS來寫,結果發現把Sleep打開會一睡不醒...... 為了能讓我用BLE+Sleep,只好切回去原廠的SDK。
可是原廠的SDK BLE超難用,最後只好開一個BLE example然後硬改成我要的。附帶一提BLE功率不高。

模組的pinout也非常的糟糕,即便是parallel bus特別安排到GPIO0~7,實際上模組腳位可能東一個西一個,搞得Layout上複雜了不少。

模組附近Layout的近照,這三小pinout

原本打算用兩個CR2032拚幾個月,結果因為螢幕本身就要吃掉500uA了其實做不到,所以還是回頭用Li-ion。電路板其他的基本上就是UART隔離+Sparkfun Bootloader的電路,還有鋰電池充電+降壓模組,最後就是幾個電流的測試點。降壓模組我這邊用的是TPS82740A,看了很久想說來試試看,這個在低耗電量(數百uA)的範圍轉換效率真的很高,Iq也耗很少電,BGA封裝但是很好Layout和焊接,最高頂多輸出200mA但是這種用途非常足夠了。
鋰電池電壓監測照慣例用隔離電路,平常斷開分壓電阻,只有需要的時候才打開一下。

EN pin低到高會打開這個MOS 5ms,這時候Sample VBAT。

Apollo3b 其實內建了類似的電路,而且還有一個500歐姆的load resistor,透過比較有load/無load的電壓去計算電池內阻來判定電池容量,滿狂的。只可惜螢幕只能跑3.3V,我也不想另外放電平轉換電路在8bit匯流排上面。

接下來就是畫圖軟體了,先來介紹第一種用途:畫天氣圖
天氣圖基本上就是RPI資料載下來-->轉檔-->BLE傳Frame Buffer到板子上。
剛剛提到一張Frame也將近56K,所以這裡勢必是需要一點壓縮,原本找了老半天看了老半天的黑白影像壓縮,看來大家都很愛RLE(running length encoding)就跟著.....沒有,用RLP壓完變得更肥。最後覺得乾脆gzip算了結果壓縮比還不錯,上面這張圖壓完只剩下21Kb。
MCU上面的解壓縮用的是這個Library: https://github.com/pfalcon/uzlib
解壓縮基本上照著example就好,只是我開了32K給他放dictionary: 
    uzlib_uncompress_init(&d,(void*)dict, 32768);

解壓縮直接解到Frame buffer然後寫進去Flash,因為天氣圖有未來兩天半的資料一共七張圖,所以我直接把Flash後面的512K都拿來寫,每次傳之前就把這512K一次清掉。

資料抓NOAA Short Range Forecasts: https://www.wpc.ncep.noaa.gov/basicwx/basicwx_ndfd.php
透過Python把時間點和圖片抓下來,另外多虧這個圖片是256色的gif,透過替換顏色的方式把地圖,等壓線,文字,鋒面給擷取出來。

這邊算是貼給我自己看得,這段Python包含爬蟲,下載,畫圖,壓縮,上傳BLE
from bluepy import btle
from bluepy.btle import Scanner, DefaultDelegate
import os
import numpy
from PIL import Image
import PIL.ImageOps as ImageOps
import binascii
from io import BytesIO
import gzip
import time
import requests
from PIL import ImageDraw
from PIL import ImageFont
import requests
from pyquery import PyQuery

os.system('rm *.gif')

class ScanDelegate(DefaultDelegate):
    def __init__(self):
        DefaultDelegate.__init__(self)

scanner = Scanner().withDelegate(ScanDelegate())
devices = scanner.scan(8.0)

nameToMacAddress = dict()
for dev in devices:
    #print("Device %s (%s), RSSI=%d dB" % (dev.addr, dev.addrType, dev.rssi))
    for (adtype, desc, value) in dev.getScanData():
        #print("  %s = %s" % (desc, value))
        if desc == "Complete Local Name":
            nameToMacAddress[value] = dev.addr;

print("Connecting...")

dev = btle.Peripheral(nameToMacAddress["SharpMeMoryLC"])
time.sleep(1)
dev.setMTU(2049)
lightSensor = btle.UUID("00002760-08c2-11e1-9073-0e8ac72e1011")
DataService = dev.getServiceByUUID(lightSensor)
uuidConfig = btle.UUID("00002760-08c2-11e1-9073-0e8ac72e0011")
BLE_data = DataService.getCharacteristics(uuidConfig)[0]

def binarize_array(numpy_array):
    RED = numpy.array([255, 0, 0])
    BLUE = numpy.array([0, 0, 255])
    BLACK = numpy.array([0, 0, 0])
    GRAY = numpy.array([220, 220, 220])
    RED2 = numpy.array([238, 44, 44])
    PURPLE = numpy.array([145, 44, 238])
    WHITE = numpy.array([255, 255, 255])
    red, green, blue = numpy_array.T
    redAreas = (red == 255) & (blue == 0) & (green == 0)
    blueAreas = (red == 0) & (blue == 255) & (green == 0)
    blackAreas = (red == 0) & (blue == 0) & (green == 0)
    grayreas = (red == blue) & (blue == green) & (green<255)
    red2Areas = (red == 238) & (blue == 44) & (green == 44)
    purpleAreas = (red == 145) & (blue == 44) & (green == 238)
    output = numpy.full(numpy_array.shape,255, dtype=numpy.uint8)
    output[redAreas.T] = (0, 0, 0)
    output[blueAreas.T] = (0, 0, 0)
    output[blackAreas.T] = (0, 0, 0)
    output[grayreas.T] = (0, 0, 0)
    output[red2Areas.T] = (0, 0, 0)
    output[purpleAreas.T] = (0, 0, 0)
    return output

def round_corner(radius):
    """Draw a round corner"""
    corner = Image.new('1', (radius, radius), (1))
    draw = ImageDraw.Draw(corner)
    draw.arc((0, 0, radius * 2, radius * 2), 180, 270, fill=(0),width=3)
    return corner

def round_rectangle(size, radius,timeText):
    """Draw a rounded rectangle"""
    width, height = size
    rectangle = Image.new('1', size, (1))
    corner = round_corner(radius)
    rectangle.paste(corner, (0, 0))
    rectangle.paste(corner.rotate(90), (0, height - radius - 3))  # Rotate the corner and paste it
    rectangle.paste(corner.rotate(180), (width - radius -3, height - radius -3))
    rectangle.paste(corner.rotate(270), (width - radius-3, 0))
    ImageDraw.Draw(rectangle).line((radius,3, width - radius, 3), fill=(0),width=3)
    ImageDraw.Draw(rectangle).line((radius,height - 3, width - radius, height - 3), fill=(0),width=3)
    ImageDraw.Draw(rectangle).line((3,radius, 3, height - radius), fill=(0),width=3)
    ImageDraw.Draw(rectangle).line((width - 3,radius, width - 3, height - radius), fill=(0),width=3)
    fnt = ImageFont.truetype('./FreeMonoBold.ttf', 22)
    x,y = ImageDraw.Draw(rectangle).textsize(timeText,font=fnt)
    ImageDraw.Draw(rectangle).text((int((width-x)/2), int((height-y)/2)),timeText,(0),font=fnt)
    return rectangle


baseURL = 'https://www.wpc.ncep.noaa.gov'
r = requests.get('https://www.wpc.ncep.noaa.gov/basicwx/basicwx_ndfd.php')
pq = PyQuery(r.text)
periodList = ['period1','period2','period3','period4','period5','period6','period7']
filelist = []
timeList = []
for period in periodList:
    tag = pq('div#'+period+' img')
    if len(tag) == 0:
        continue
    fileName = tag.attr['src'].split('/')[2]
    timeName = tag.attr['alt'].split(' ')[-1] + " " + tag.attr['alt'].split(' ')[-2][0:3]
    print('FileName: %s, Time: %s' % (fileName,timeName))
    os.system('wget ' + baseURL + tag.attr['src'])
    filelist.append(fileName)
    timeList.append(timeName)

count = 0
for filename in filelist:
    print(filename)
    image = Image.open(filename)
    #label = image.crop((393, 659, 393+250, 712))
    label = image.crop((437, 684, 437+55, 684+12))
    #print(pytesseract.image_to_string(label, lang='eng'))
    #print(pytesseract.image_to_data(label))
    label = label.resize((100,21))
    label = label.convert('L')
    label = label.convert('1')
    image = image.resize((800,558))
    image = image.convert('RGB')
    pix = numpy.array(image)
    pix = binarize_array(pix)
    image = Image.fromarray(pix)
    image = image.convert('L')
    image = image.convert('1')
    image2 = Image.new('1',(800,600),(1))
    image2.paste(image, (0, 41))
    rect = round_rectangle((110,42),10,timeList[count])
    image2.paste(rect, ((800/len(filelist))*count, 0))
    #image2.paste(label,(110*count + 5, 10))
    count = count + 1
    image2.save(filename + '.bmp')
    pix = numpy.array(image2)
    bitArray = bytearray()
    for x in pix:
        row_int = numpy.packbits(numpy.uint8(x))
        for y in row_int:
            bitArray.append(y)
            pass
    out = open(filename+'.gz','wb')#BytesIO()
    with gzip.GzipFile(fileobj=out, mode="wb") as f:
      f.write(bitArray)
    out.close()

BLE_data.write(b'\xFE')

for filename in filelist:
    print("Sending %s" % filename)
    with open(filename + ".gz", "rb") as f:
        byte = f.read(240)
        print(len(byte))
        while byte != b"":
            BLE_data.write(len(byte).to_bytes(1,'big')+byte)
            byte = f.read(240)
            #time.sleep(0.1)
        BLE_data.write(b'\x00')
    time.sleep(10)



以上就是畫天氣圖的部分,接下來就是Dashboard:

畫圖基本上還是Adafruit GFX,只是因為SDK是純C所以改了一下把原本的C++變成C。
而Dashboard的Prototype是靠AIDA64大致上抓一下感覺。
不過最難的在於自己寫這一個widget:


大概噴了兩個多小時寫了這個簡單的widget,因為Arc不好搞。
struct arc_struct {
    char plotName[15];
    float value;

    char unit[2];

    int innerR;
    int outerR;

    float max;
    float min;

    float angle;

    int origin_x;
    int origin_y;

};  
//https://stackoverflow.com/questions/8887686/arc-subdivision-algorithm/8889666#8889666
void f(float cx, float cy, float px, float py, float theta, int N)
{
    float dx = px - cx;
    float dy = py - cy;
    float r2 = dx * dx + dy * dy;
    float r = sqrt(r2);
    float ctheta = cos(theta/(N-1));
    float stheta = sin(theta/(N-1));
    //std::cout << cx + dx << "," << cy + dy << std::endl;
    for(int i = 1; i != N; ++i)
    {
        float dxtemp = ctheta * dx - stheta * dy;
        dy = stheta * dx + ctheta * dy;
        dx = dxtemp;
        writePixel(cx + dx, cy + dy, BLACK);
        //std::cout << cx + dx << "," << cy + dy << std::endl;
    }
}
void plotArc(struct arc_struct *plot){

    plot->angle = plot->value / (plot->max - plot->min) * 360;

    for(float x=plot->innerR;x<plot->outerR;x+=0.5){
        f(plot->origin_x, plot->origin_y, plot->origin_x, plot->origin_y-x, plot->angle/180*3.14,350);
    }
    drawCircle(plot->origin_x, plot->origin_y,plot->innerR-2,BLACK);
    drawCircle(plot->origin_x, plot->origin_y,plot->outerR+1,BLACK);
    setTextColor(BLACK);
    setTextSize(3);
    char buf[20]={0};
    int n = sprintf(buf,"%.0f%s",plot->value,plot->unit);
    int16_t x1,y1,w,h;
    getTextBounds(buf,0,0,&x1, &y1,&w,&h);
    setCursor(plot->origin_x-w/2,plot->origin_y-h/2);
    for(int i=0;i<n;i++){
        LCDwrite(buf[i]);
    }

    setTextSize(2);
    n = sprintf(buf,"%s",plot->plotName);
    getTextBounds(buf,0,0,&x1, &y1,&w,&h);
    setCursor(plot->origin_x-w/2,plot->origin_y+plot->outerR+18-h/2);
    for(int i=0;i<n;i++){
        LCDwrite(buf[i]);
    }

}

其實效率非常差,我的畫法是一堆Arc填滿,但是應該要用Boundary + Fill algorithm,之後再來改進吧。其他Widget就還好了,都還算簡單,Code滿亂的改天整理完再Po。

耗電量這邊也可以看到畫圖花了非常久,大概400ms才畫完。
平均耗電量約莫1.3mA,不低但是搭配2500mAh的電池也是有兩個多月的使用時間。

資料靠HWiNFO + Python Script,HWiNFO 可以把資料導向一個UDP port,然後Python去收。
這邊有人寫好了一個範例: https://medium.com/swlh/reverse-engineering-a-tcp-protocol-455d248d68fa,我只是拿來改

只是BLE連線我目前暫時是用RPI Zero 來處理,然後透過USB Ethernet device直接連到我的PC。

大概就是這樣,順帶一提買了一台新的3DP MK3S+真的好好用,按鍵一按下去電腦的3D模型就生出來了呢,這次的外殼就是新3D印表機弄的,連螺絲都是組裝剩下的備料XD

2021年1月1日 星期五

Solar Eclipse 2020

 


2020年算是我個人變動比較大的一年,去年出國念書今年畢業在國外上工,經歷了Remote Learning的下學期,沒有畢業典禮+計畫被打亂的畢業,在空蕩蕩的機場搭飛機回台灣隔離14天,然後又飛回去搬家準備上工。一直沒什麼時間寫下這篇記錄,趕在2020年要過了之前寫完這篇。

這篇算是在防疫旅館做完的Project,目標是做出一個自動追蹤太陽的照相機來照日環蝕。通常要追蹤星體都是用赤道儀,透過跟地球自轉同步的旋轉軸達到跟星體同步,但是沒有用赤道儀有兩個原因:

  1. 一個是赤道儀貴得要死......貴到不好意思跟老師借來玩
  2. 第二個是由於地球自轉軸是傾斜的,赤道儀使用之前需要手動對齊北極星+周圍幾個參考星體(因為北極星不是正北),我不確定白天是要怎麼對齊

所以就變這樣了:


CCD(CMOS? lol)是Raspberry Pi HQ Camera,雖然比不上單眼但是很夠用了,加上因為底片大小比較小(1/2.3)所以長焦的鏡頭也比較小,勉強可以用便宜的兩軸伺服器馬達來移動。
長焦鏡頭是Canon FD 100-200 MM f5.6 Lens,RPI HQ Camera一個特色是因為是C/CS mount,所以可以轉接不少老/新鏡頭,我翻了一下Ebay找Cannon FD的長焦就這個好像還可以,買起來才13USD。

好,所以到此照相/機械架構有了,接下來就軟體的架構: 目標是做出一個能夠收集完整解析度之下最高FPS最原始的資料+自動追蹤的架構。

RPI相機資料有四種輸出: 1. MJEPG Encoder 2.H.264 Encoder 3. Raw 4.YUV
大致上整體影像的Pipe Line如下: RAW----->Debayer---->MJEPG or H.264 Encoder
四種輸出就是看你從哪一個Process接出來用

其中H.264有FHD解析度限制,所以只剩下1和3,MJEPG的問題是RPI的Video Encoder不知道為啥Quality調得很低,壓出來的畫質慘不忍睹,第二個問題是MJEPG Encoder雖然沒有解析度限制,但是也是有總pixel的處理限制,雖然解析度不至於頂到但是假如我開VNC調用MJEPG的時候就會撞到。於是只剩下RAW和YUV,RAW檔案的問題是因為是相機底層的資料,我需要先生出相機校正的參數,包含鏡頭變形和色散等等的,老實說我在旅館做不來,加上資料量大小真的恐怖,所以最後試了一下還是用YUV資料。

但是YUV資料量也很恐怖,假設以頂到RPI HQ Camera的CSI-2頻寬上限來算,10 FPS x 4056 x 3040 x 2 = 30Mb/s,要記錄全程大概數小時的資料至少就要數百GB,所以勢必是需要有壓縮的方式,加上太陽濾鏡之後的資料其實就一個大圓以外都黑的,簡單的JPEG就很能壓縮。

我試了在RPI上面跑FFMPEG的結論就是RPI4做不來,那只剩下一個方式:把資料Dump到我的筆電處裡,透過1G的有線網路的話至少可以跑到5FPS,於是就用了raspividyuv+FFMPEG在我的電腦收RPI Dump的資料。

這邊還有一個需要留意的,RPI的ISP生出Frame的時候會帶有timestamp,通常是為了送到Encoder做時間軸用而raspividyuv不會用到,但是因為日蝕是有精確的時間點的,我需要知道每個Frame的時間才好處理,所以我稍微修改了一下raspividyuv把pts紀錄到另外一個檔案裡,事後再把Frame+PTS一同處理。

插曲就是2.5G有線網路其實差不多要開始流行了,我看到2.5G的USB Dongle價錢開始往下壓就買了兩隻想說這下我終於可以把1G的瓶頸暴力的拉到2.5(幾乎是RPI HQ Cam的CSI-2頻寬),結果就是預設的Linux Driver跑不到2.5,很微妙的在1.2左右,原廠的Driver不知道為啥也安裝不了但是還是比1G好一點就硬著頭皮用了

ISP除了Debayer以外還有其他2A的功能,包含Auto Brightness + AWB,Auto Focus因為RPI Camera沒有調焦的功能,所以只能輸出Focus的數字,所以在Setup的時候是先跑輸出Focus找到對焦點之後再開始資料採集。而由於日蝕的亮度變化,所以Auto Brightness有開,而AWB則是固定,我的Cmd如下:
raspividyuv -t 0 -fps 15 --mode 3 -w 4056 -h 3040 -ss 10000 -ISO 100 -ev -10 -mm average -ex spotlight -awb sun -o tcp://169.254.142.200:6667 -pts $DATE_STRING.txt -v
./ffmpeg.exe -f rawvideo -pix_fmt yuv420p -s 4064x3040 -probesize 89M -r 10 -i tcp://0.0.0.0:6667?listen -q:v 5 ".\$((Get-Date).ToString('yyyy-MM-dd_hh-mm-ss'))\img-%08d.jpg" -vf scale=800:600,fps=1 -q:v 2 -f image2 -update 1 udp://169.254.35.220:13991
加上我懶,所以Setup Stream是用power shell script自動跑這兩隻程式
*Note: YUV輸出的時候會自動Padding所以FFMPEG收的時候是4064x3040 

以上是資料採集,接下來是自動追蹤
我其實以前也做過太陽的追蹤,2017美西有一次Solar Eclipse,雖然我人在加州只看到日偏食,但是還是有照全程,只是那時候我忘記帶腳架,導致事後資料處裡很麻煩,於是就寫了OpenCV的程式自動擷取太陽出來:
2017年LA日偏食

效果不錯,因為太陽濾鏡之後也只剩下一個大圓球很好追,所以就用了OpenCV來追太陽
如果稍微看ffmpeg的指令的話會發現除了壓縮完影像之後,其實我還Copy了Frame然後把解析度調到800x600後送回RPI4,這個資料就是回去給RPI4上面的OpenCV用的,OpenCV抓到太陽的中心點之後就會判斷需不需要移動(因為伺服器馬達有角解析度的限制),然後送指令給伺服器馬達的控制Server。最後為了我自己控制的方便,挖了XBOX的控制器出來讓我能直接手動找太陽+開/關Auto Control。

當然這還是得要先測試個幾次:
很蠢的測試



以下是這次日蝕的OpenCV的追蹤結果

有雲的時候邊緣比較拿抓閥值,但是可以直接指定圓的大小就還好,重點是中心點

當然還是會有跑掉的時候,所以控制Loop需要有一點De-bounce

因為下一張就回去了




當天跑去故宮南院,還搬了冰桶過去XD



從早上待到結束,Some Result:
這張是NEX-5N照底圖+RPI HQ Camera Overlay上去的,照底圖要在太陽來之前/之後照XD

幾張Key Frame

以上,就是在這2020年顛簸的旅程中的一點紀錄





2019年6月1日 星期六

SI8B Pancake Geiger Counter Tube


速戰速決,總之這是新買的一個蓋格管,從圖來看最大的特色就是這個設計的感應面積非常大,通常暱稱叫做Pancake Tube。多虧這個大片的感應面積,這個蓋格管比之前我用管狀的靈敏很多,這款的背景CPM通常可以到~180

而且最有趣的是因為上蓋是透明的雲母片,所以其實可以看的到輻射打過去所激發的光。


大Guy94這樣,驅動版我懶得做就挖了之前高空氣球的那塊板子來用所以沒有特別的點,電壓我還是調成跟之前一樣,連Bais的電阻都是1M而已,短時間照照相可能還好,長時間拿來做監控可能還是要照一下規格書給4.7M。



2019年4月27日 星期六

LoRa Gateway / Concentrator V2


總之這是第二版的LoRa Gateway。

說來話長,不過我當初做LoRa Gateway並不是要用來接LoRawan,而是跑自己的系統去收探空儀的資料,可是最後做出來之後因為要整合進現有系統的時間和成本都太高,所以最後來不及放在探空儀系統裡面。(當然啦,論文一直沒有出來我也不想繼續幫忙下去)

所以這版最主要就是把這個Project修正回適合LoRawan部屬的東西,簡單來說就是把PA/LAN和RF Switch做上去,從上圖和之前的比較就可以發現這片只有一個RF出來接天線,其他的都在板子上了。

Layout上除了保持RF走線能夠跑越短越好以外,TX/RX的Stage也隔的很遠,圖的上方是PA Stage,下方則是LNA,另外也換成了四層板,所以RF Switch和PA power switch都可以透過layer 4走回來而不破壞原有的GND Plane。

然後這東西在設計快到結尾的時候剛好RPI 3 B+出來,雖然了無新意(因為被VC綁死SOC其實也沒啥把戲可以玩了),但是為了PoE我還是買單了,因為真的在部屬一個戶外的Node其實要網路要供電的話,PoE一條網路線可以牽個幾十公尺都沒問題,而且搭配工業用供電器真的方便。所以我就立刻把PoE Header做上去了,原本其實也是想把PoE模組做上去,只是有個小問題,除了Layout會需要大改以外,通常常見的PoE模組供電量會因為電感限制在2A,也就是要嘛就是12V2A要嘛就是5V2A,要再上去也有只是貴了一階上去,我當時不確定拿5V2A夠不夠就先放棄了,只把POE拉出來讓上層可以用RPI出的PoE Hat。

其他的就差不多了,四層板另外換成霧面黑(我真的愛霧面黑)

除此之外由於換成了四層板,我回頭去看了一下RPI Zero的版本.....

其實真的擠得進去XDDDD

好啦,這不完全是一樣的設計,應該有人發現到RFFE只剩下一顆,我其實用了一顆PA/LNA/Switch整合的RF Frontend,所以可以把面積再次往下縮小,就變上圖那樣了,而且SX1301的133Mhz Crystal也找了一顆非常小的2520面積的OSC,整個就是壓面積


四層板用到連電源走線都有點難度的那種狀況,但是還是維持了一整片完整的GND Plane就是
少數幾張我覺得很誇張的板子


最後就是來講一下一個應該沒有人注意到的點,有看到第一張電路板上面有個TW0001嗎?
這其實是OSHW的認證標示。Opensource Hardware這件事其實很多人會宣稱自己是,可是常常發生這種明明宣傳是,可是只放出來一張電路原理圖或者是一張BOM表這種,所以OSHW基本上就是設計了一套新的認證機制,就是讓設計者去登記自己的作品,然後透過填寫的問卷去評估是否符合OSHW的要求。

我基本上覺得台灣很多人在談論OSHW的時候基本上是在講幹話,因為他根本沒有Release什麼鬼東西就宣稱自己是OSHW,不然就是對這種新的認證方式根本不了解而只是一昧地去宣傳一種空泛的OSHW的口號而不去散佈正確的OSHW的Criteria。我其實等了半年都沒看到OSHW有台灣的人去登記覺得有點傷心,最後就默默的把我的這個作品拿去登記了。中間也不是沒有去跟其他人宣傳過這件事,可惜的是絕大多數的台灣人都害怕東西Release出去就會有其他人來抄襲而不願意去公開。的確,科技業這類保護自己的方式當然沒有甚麼問題,可是你他媽的就不要宣稱自己是OSHW阿阿阿阿

只能說是科技業的心態已久,然後宣傳的人大多也只是提倡一種空泛的口號。
台灣第一個OSHW Certified Project


覺得台灣大多數在談論OSHW其實也只是徒有其表而無其實就是。滿紙荒唐言,謹以此紀錄在我的筆記本裡面。