實踐:移植復(fù)古計算器到M5Stack Core2)
1. 項目緣起當(dāng)復(fù)古計算器遇上現(xiàn)代硬件最近在折騰一個挺有意思的小玩意兒把一款名為“Galdeano”的復(fù)古計算器程序移植到了M5Stack Core2這塊小巧的ESP32開發(fā)板上。這事兒聽起來可能有點小眾但整個過程充滿了樂趣和挑戰(zhàn)也讓我對嵌入式開發(fā)、UI交互以及如何讓“老物件”在“新平臺”上煥發(fā)生機有了更深的理解。如果你也對復(fù)古計算器、嵌入式圖形界面開發(fā)或者單純想找點有格調(diào)的DIY項目來玩這篇分享或許能給你帶來一些啟發(fā)。Galdeano本身是一款設(shè)計非常優(yōu)雅的模擬計算器它的界面和交互邏輯充滿了上世紀(jì)七八十年代經(jīng)典計算器的美學(xué)風(fēng)格。而M5Stack Core2則是一款集成了彩色觸摸屏、豐富傳感器和強大ESP32主控的現(xiàn)代化開發(fā)套件。將這兩者結(jié)合本質(zhì)上是在用一塊性能遠(yuǎn)超當(dāng)年任何計算器的芯片去復(fù)現(xiàn)一種純粹、專注的計算體驗。這不僅僅是功能的實現(xiàn)更是一種對設(shè)計美學(xué)的致敬和再創(chuàng)造。整個項目涉及從固件編譯、圖形庫適配、觸摸交互處理到電源管理等多個環(huán)節(jié)接下來我就把踩過的坑和收獲的經(jīng)驗詳細(xì)拆解一遍。2. 核心組件選型與項目定位在動手之前明確我們手頭的“積木”和要搭建的“建筑”至關(guān)重要。這個項目的核心是軟件在特定硬件上的完美呈現(xiàn)因此對軟硬件的透徹理解是第一步。2.1 M5Stack Core2硬件平臺解析M5Stack Core2不是一塊簡單的開發(fā)板它是一個高度集成、開箱即用的解決方案。選擇它作為載體主要基于以下幾點考量顯示與交互核心其搭載的2英寸IPS電容觸摸屏320x240分辨率是項目的視覺和交互基礎(chǔ)。分辨率足夠清晰展現(xiàn)計算器的細(xì)節(jié)電容觸摸提供了比電阻屏更流暢的點擊和滑動體驗這對于模擬實體按鍵的按壓反饋至關(guān)重要。足夠的性能儲備ESP32-D0WD雙核處理器主頻高達(dá)240MHz配合520KB的SRAM運行一個計算器模擬器綽綽有余。但這恰恰是難點所在——如何讓一個功能簡單的應(yīng)用在性能富裕的平臺上依然保持高效、低功耗并且響應(yīng)迅速這需要精細(xì)的代碼控制。完善的生態(tài)與供電板載的AXP192電源管理芯片、振動馬達(dá)、揚聲器、SD卡槽等為項目擴展提供了無限可能。例如可以通過振動馬達(dá)模擬按鍵的物理觸感用揚聲器播放經(jīng)典的按鍵音效甚至利用SD卡存儲計算歷史或不同的皮膚主題。內(nèi)置的390mAh鋰電池也保證了設(shè)備的便攜性。注意M5Core2的屏幕驅(qū)動芯片是ILI9342C這與常見的ILI9341在初始化序列上略有不同。如果你直接使用針對ILI9341的庫可能會遇到屏幕初始化失敗或顯示異常的問題這是移植過程中第一個需要確認(rèn)的細(xì)節(jié)。2.2 Galdeano計算器的軟件靈魂Galdeano并非一個我們熟知的商業(yè)軟件它更像是一個來自極客社區(qū)的開源藝術(shù)項目。通常它可能以以下幾種形式存在一個獨立的、針對特定嵌入式平臺如Game Boy Pebble手表的應(yīng)用程序源碼結(jié)構(gòu)緊湊直接操作幀緩沖區(qū)Framebuffer進(jìn)行繪圖。一個基于特定圖形庫如LVGL U8g2的示例程序其UI控件和邏輯與圖形庫深度綁定。一個純粹的、平臺無關(guān)的計算邏輯核心庫只負(fù)責(zé)數(shù)學(xué)運算和狀態(tài)管理需要自行實現(xiàn)前端界面。在開始移植前必須搞清楚你找到的Galdeano源碼屬于哪一種。這決定了移植的工作量和技術(shù)路徑。我這次處理的是一個基于簡單幀緩沖區(qū)的版本這意味著我需要自己實現(xiàn)或適配一個圖形庫來驅(qū)動M5Core2的屏幕。2.3 技術(shù)路徑選擇從頭造輪子還是站在巨人肩上面對M5Core2我們有幾種圖形方案可選直接使用M5Stack官方庫M5Core2這是最直接的方式。官方庫封裝了屏幕、觸摸、電源等所有硬件的驅(qū)動提供了高級的圖形API如繪制矩形、文字、圖片。優(yōu)點是集成度高上手快。缺點是API相對底層構(gòu)建復(fù)雜的、有動態(tài)效果的UI如按鍵按下動畫需要自己管理狀態(tài)代碼量會比較大。引入LVGL等嵌入式GUI庫LVGL是一個功能強大的開源嵌入式圖形庫支持控件、動畫、主題、事件驅(qū)動。如果Galdeano的UI交互比較復(fù)雜或者你希望未來能輕松更換皮膚、添加新功能使用LVGL是更可持續(xù)的選擇。但這也意味著你需要將Galdeano的業(yè)務(wù)邏輯“掛載”到LVGL的事件回調(diào)體系中有一定學(xué)習(xí)成本。使用TFT_eSPI等輕量級驅(qū)動庫TFT_eSPI驅(qū)動效率很高但主要提供繪圖原語。你需要完全自己管理整個UI的繪制和刷新適合對性能和代碼體積有極致要求且UI固定的場景。我的選擇是方案一即基于M5Core2庫進(jìn)行開發(fā)。原因在于Galdeano的界面雖然是復(fù)古的但元素和交互是確定的、靜態(tài)的按鍵、顯示屏。使用官方庫可以避免引入額外的依賴減少復(fù)雜度讓我更專注于將計算器邏輯與觸摸事件精準(zhǔn)對接。對于按鍵反饋我通過M5.Lcd.fillRoundRect快速重繪按鍵區(qū)域并改變顏色來模擬按下效果配合M5.Axp.SetVibration控制振動馬達(dá)體驗已經(jīng)足夠好。3. 開發(fā)環(huán)境搭建與基礎(chǔ)工程創(chuàng)建工欲善其事必先利其器。一個穩(wěn)定高效的開發(fā)環(huán)境能避免很多低級錯誤。3.1 Arduino IDE與板卡支持配置雖然PlatformIO更強大但Arduino IDE對于快速原型驗證依然非常友好。步驟如下安裝Arduino IDE從官網(wǎng)下載并安裝最新穩(wěn)定版。添加ESP32板卡支持打開“文件”-“首選項”在“附加開發(fā)板管理器網(wǎng)址”中輸入https://espressif.github.io/arduino-esp32/package_esp32_index.json。然后打開“工具”-“開發(fā)板”-“開發(fā)板管理器”搜索“esp32”安裝“Espressif Systems”提供的包。安裝M5Stack庫打開“工具”-“管理庫...”搜索“M5Core2”安裝由“M5Stack”官方發(fā)布的庫。安裝后你可以在示例中找到很多參考代碼。3.2 項目工程結(jié)構(gòu)與核心代碼文件創(chuàng)建一個新的Arduino項目我建議建立如下結(jié)構(gòu)的文件夾雖然Arduino IDE主要管理.ino文件但良好的本地文件管理有助于代碼組織Galdeano_M5Core2/ ├── Galdeano_M5Core2.ino (主程序入口) ├── CalculatorEngine.h / .cpp (計算器核心邏輯移植或重寫Galdeano的運算部分) ├── UI.h / .cpp (用戶界面繪制與狀態(tài)管理) ├── TouchHandler.h / .cpp (觸摸事件處理與映射) └── assets/ (存放字體、按鍵圖片等資源可選)在Galdeano_M5Core2.ino中基礎(chǔ)的框架如下#include M5Core2.h #include CalculatorEngine.h #include UI.h #include TouchHandler.h CalculatorEngine calcEngine; UI ui(M5.Lcd); // 傳入屏幕對象 TouchHandler touchHandler; void setup() { M5.begin(true, true, true, true); // 初始化M5Core2啟用LCD、SD卡、I2C、串口 M5.Lcd.setRotation(1); // 根據(jù)你的握持習(xí)慣設(shè)置屏幕方向1或3 ui.init(); // 初始化UI繪制靜態(tài)界面 calcEngine.reset(); // 重置計算器狀態(tài) } void loop() { M5.update(); // 更新按鈕、觸摸等狀態(tài)必須調(diào)用 // 檢查觸摸事件 if (M5.Touch.ispressed()) { Point touchPoint {M5.Touch.getX(), M5.Touch.getY()}; int keyPressed touchHandler.mapTouchToKey(touchPoint, ui.getLayout()); if (keyPressed ! KEY_NONE) { // 處理按鍵更新UI反饋調(diào)用計算引擎 ui.drawKeyPressed(keyPressed); calcEngine.processKey(keyPressed); ui.updateDisplay(calcEngine.getDisplayValue()); delay(50); // 簡單的防抖和視覺反饋延時 ui.drawKeyReleased(keyPressed); } } // 這里可以添加其他任務(wù)如低功耗睡眠管理 // 如果長時間無操作可以關(guān)閉屏幕背光 static uint32_t lastActiveTime millis(); if (millis() - lastActiveTime 30000) { // 30秒無操作 M5.Axp.SetLcdVoltage(2500); // 大幅降低背光電壓近乎關(guān)閉 // 進(jìn)入輕睡眠等待觸摸中斷喚醒 } }這個框架清晰地分離了顯示UI、邏輯CalculatorEngine和輸入TouchHandler符合模塊化設(shè)計原則。4. 計算器核心邏輯的移植與重構(gòu)這是項目的“大腦”。原始的Galdeano代碼可能充斥著全局變量和冗長的條件判斷。我們的目標(biāo)是將它重構(gòu)為一個狀態(tài)清晰、接口明確的類。4.1 狀態(tài)機設(shè)計理解計算器的工作流一個標(biāo)準(zhǔn)計算器的核心是一個狀態(tài)機。它需要跟蹤當(dāng)前顯示值currentDisplay屏幕上顯示的數(shù)字字符串。上一個操作數(shù)previousOperand和當(dāng)前操作數(shù)currentOperand。待執(zhí)行的操作pendingOperation如加()、減(-)、乘(*)、除(/)。狀態(tài)標(biāo)志是否剛按了等號equalsPressed、是否在輸入小數(shù)inputtingDecimal、小數(shù)位數(shù)decimalPlaces等。在CalculatorEngine.h中我們定義關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)和枚舉#ifndef CALCULATOR_ENGINE_H #define CALCULATOR_ENGINE_H enum CalcKey { KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_DOT, KEY_CLEAR, KEY_CLEAR_ENTRY, KEY_ADD, KEY_SUB, KEY_MUL, KEY_DIV, KEY_EQUALS, KEY_SIGN, KEY_PERCENT, KEY_NONE }; class CalculatorEngine { public: CalculatorEngine(); void processKey(CalcKey key); const char* getDisplayValue() const; void reset(); private: double previousOperand; double currentOperand; CalcKey pendingOperation; bool isNewOperand; // 標(biāo)志是否開始輸入新操作數(shù) bool inputtingDecimal; int decimalPlaceCounter; char displayBuffer[16]; // 顯示緩沖區(qū) void handleDigit(CalcKey key); void handleOperator(CalcKey key); void handleEquals(); void handleClear(); void updateDisplayBuffer(); double performCalculation(double a, double b, CalcKey op); }; #endif4.2 關(guān)鍵算法實現(xiàn)連續(xù)運算與優(yōu)先級處理計算器最微妙的部分在于連續(xù)運算的邏輯。例如輸入3 4 x 5 不同計算器有不同結(jié)果是(34)*535還是3(4*5)23。大多數(shù)基礎(chǔ)計算器采用立即執(zhí)行邏輯遇到新運算符時立即執(zhí)行上一個運算符。而科學(xué)計算器會考慮運算符優(yōu)先級。Galdeano作為復(fù)古風(fēng)格計算器很可能采用前者以保持其時代特性。在handleOperator函數(shù)中我們需要實現(xiàn)這種邏輯void CalculatorEngine::handleOperator(CalcKey op) { // 如果已有待執(zhí)行的操作先執(zhí)行它立即執(zhí)行邏輯 if (pendingOperation ! KEY_NONE !isNewOperand) { handleEquals(); // 內(nèi)部執(zhí)行 previousOperand performCalculation(previousOperand, currentOperand, pendingOperation); } else { // 如果沒有前一個操作則將當(dāng)前顯示值作為上一個操作數(shù) previousOperand currentOperand; } // 更新待執(zhí)行操作并標(biāo)記開始輸入新操作數(shù) pendingOperation op; isNewOperand true; inputtingDecimal false; decimalPlaceCounter 0; }performCalculation函數(shù)則相對直接但要注意除零錯誤和浮點數(shù)精度問題。對于顯示我們通常需要格式化數(shù)字比如限制小數(shù)位數(shù)、處理過大數(shù)字顯示為科學(xué)計數(shù)法或“E”錯誤。實操心得浮點數(shù)精度是計算器永遠(yuǎn)的痛。直接使用double進(jìn)行連續(xù)運算尤其是涉及小數(shù)時可能會產(chǎn)生0.1 0.2 ! 0.3這樣的顯示問題。一種解決方案是內(nèi)部使用整數(shù)運算通過記錄小數(shù)點位置來模擬小數(shù)。例如將12.34存儲為整數(shù)1234和小數(shù)點位置2。這對于四則運算計算器來說更精確但實現(xiàn)起來稍復(fù)雜。我為了保持與原始Galdeano風(fēng)格的接近暫時使用了double但在顯示時做了四舍五入到固定位數(shù)如10位的處理并在除零時顯示“Error”。5. 用戶界面繪制與觸摸交互實現(xiàn)UI是項目的“臉面”直接決定用戶體驗。我們的目標(biāo)是在320x240的屏幕上精準(zhǔn)復(fù)現(xiàn)Galdeano的視覺風(fēng)格。5.1 屏幕布局規(guī)劃與坐標(biāo)計算首先需要在紙上或繪圖軟件里規(guī)劃好布局。典型的計算器界面分為顯示區(qū)和按鍵區(qū)。顯示區(qū)通常位于頂部占屏幕高度的20%-25%。需要預(yù)留足夠的空間來顯示長數(shù)字和運算符號。按鍵區(qū)采用網(wǎng)格布局。例如一個4x5的網(wǎng)格每個按鍵大小相同中間留有細(xì)小的縫隙。在UI.cpp的init()函數(shù)中我們計算出每個按鍵的精確矩形坐標(biāo)并存儲在一個數(shù)組或結(jié)構(gòu)體中。TouchHandler將使用相同的布局?jǐn)?shù)據(jù)來映射觸摸點。// 示例定義按鍵布局 struct KeyLayout { CalcKey key; int x, y, w, h; const char* label; uint16_t colorNormal, colorPressed; }; KeyLayout keyLayouts[] { {KEY_7, 10, 70, 60, 40, 7, TFT_DARKGREY, TFT_LIGHTGREY}, {KEY_8, 75, 70, 60, 40, 8, TFT_DARKGREY, TFT_LIGHTGREY}, {KEY_9, 140, 70, 60, 40, 9, TFT_DARKGREY, TFT_LIGHTGREY}, {KEY_DIV, 205, 70, 60, 40, ÷, TFT_ORANGE, TFT_DARKORANGE}, // ... 定義所有按鍵 {KEY_EQUALS, 205, 210, 60, 40, , TFT_ORANGE, TFT_DARKORANGE} };5.2 繪制靜態(tài)界面與動態(tài)反饋UI::init()負(fù)責(zé)繪制所有靜態(tài)元素背景、按鍵邊框、按鍵標(biāo)簽。使用M5.Lcd.drawRoundRect和M5.Lcd.fillRoundRect來繪制圓角按鍵視覺上會更柔和。當(dāng)觸摸事件發(fā)生時UI::drawKeyPressed(int keyIndex)和UI::drawKeyReleased(int keyIndex)函數(shù)被調(diào)用通過改變特定按鍵矩形的填充色和重繪標(biāo)簽有時標(biāo)簽顏色也需改變來模擬按下效果。這里有一個重要的優(yōu)化點避免全屏刷新。只刷新發(fā)生變化的區(qū)域即被按下的那個按鍵。這可以通過M5.Lcd.setClipRect設(shè)置臟矩形區(qū)域來實現(xiàn)能顯著提高刷新效率讓動畫更流暢。void UI::drawKeyPressed(int keyIndex) { KeyLayout k keyLayouts[keyIndex]; M5.Lcd.fillRoundRect(k.x, k.y, k.w, k.h, 5, k.colorPressed); // 填充按下顏色 M5.Lcd.setTextColor(TFT_BLACK); // 文字顏色改為黑色 M5.Lcd.drawCentreString(k.label, k.x k.w/2, k.y k.h/2 - 6, 4); // 居中繪制標(biāo)簽 // 觸發(fā)振動馬達(dá)提供觸覺反饋可選 M5.Axp.SetVibration(true); delay(10); // 短振動 M5.Axp.SetVibration(false); }5.3 觸摸坐標(biāo)映射與防抖處理TouchHandler::mapTouchToKey函數(shù)是交互的樞紐。它接收觸摸點坐標(biāo)遍歷keyLayouts數(shù)組判斷點是否落在某個按鍵的矩形區(qū)域內(nèi)。踩坑實錄觸摸抖動與多點觸摸。電容屏的觸摸數(shù)據(jù)可能存在輕微抖動且M5Core2庫支持報告多個觸摸點。對于計算器這種應(yīng)用我們通常只關(guān)心第一個有效的觸摸點M5.Touch.getDetail(0)。此外需要實現(xiàn)簡單的防抖邏輯例如記錄上次按鍵釋放的時間在短時間內(nèi)如200ms忽略新的觸摸事件或者只有在觸摸點穩(wěn)定超過一定時間后才判定為有效按壓。這能防止因誤觸或抖動導(dǎo)致的重復(fù)輸入。int TouchHandler::mapTouchToKey(Point p, const KeyLayout* layout, int layoutSize) { static uint32_t lastPressTime 0; if (millis() - lastPressTime 200) { // 200ms防抖 return KEY_NONE; } for (int i 0; i layoutSize; i) { if (p.x layout[i].x p.x (layout[i].x layout[i].w) p.y layout[i].y p.y (layout[i].y layout[i].h)) { lastPressTime millis(); return layout[i].key; } } return KEY_NONE; }6. 系統(tǒng)優(yōu)化與功能擴展項目基本跑通后就可以考慮打磨體驗和增加一些亮眼功能了。6.1 電源管理與低功耗優(yōu)化M5Core2始終連接著電池良好的電源管理能極大提升使用體驗。除了在loop()中檢測無操作息屏外還可以使用ESP32的深度睡眠在息屏一段時間后讓ESP32進(jìn)入深度睡眠Deep Sleep僅由RTC維持少量內(nèi)存功耗可降至微安級。觸摸屏的觸摸控制器可以配置為外部喚醒源EXT0/EXT1喚醒。不過這需要更復(fù)雜的硬件中斷配置且喚醒后需要重新初始化屏幕和程序狀態(tài)實現(xiàn)起來有一定難度。對于計算器這種可能需要隨時快速使用的設(shè)備輕睡眠Light Sleep或簡單的關(guān)閉背光可能是更實用的選擇。動態(tài)調(diào)整CPU頻率在計算器閑置時可以通過setCpuFrequencyMhz(80)將CPU頻率從240MHz降至80MHz甚至更低在需要處理觸摸或運算時再恢復(fù)。這能有效降低平均功耗。6.2 視覺與聽覺反饋增強更細(xì)膩的動畫按鍵按下效果可以不只是顏色變化。嘗試實現(xiàn)一個“下沉”動畫在幾幀內(nèi)讓按鍵的填充色由淺入深或者讓按鍵的陰影偏移模擬物理按鍵被按下的立體感。這需要更精細(xì)的定時控制如使用millis()進(jìn)行非阻塞動畫更新。音效反饋M5Core2的揚聲器可以用來播放按鍵音。你可以錄制或生成一段簡短的“咔噠”聲將其轉(zhuǎn)換為一個字節(jié)數(shù)組.h文件存儲在程序內(nèi)或者放在SD卡中。當(dāng)按鍵被有效觸發(fā)時通過M5.Speaker.tone播放一個特定頻率的短音或者使用AudioFileSourceSD和AudioGeneratorWAV庫需要額外安裝播放WAV文件。音效能極大增強沉浸感。6.3 個性化與數(shù)據(jù)持久化主題切換將按鍵顏色、背景色等定義為可配置的主題結(jié)構(gòu)體。在SD卡上放置一個config.json文件程序啟動時讀取并應(yīng)用主題。用戶甚至可以自己設(shè)計主題。計算歷史記錄在內(nèi)存中維護(hù)一個環(huán)形緩沖區(qū)記錄最近10次的計算表達(dá)式和結(jié)果。通過一個特定的手勢如長按“C”鍵或新增一個“HIST”按鍵來喚出歷史記錄界面并支持選擇某條歷史記錄重新載入。這需要擴展UI和交互邏輯。通過Web界面配置讓計算器啟動一個Wi-Fi熱點用戶手機連接后可以通過一個簡單的網(wǎng)頁來配置主題、音效開關(guān)等。這利用了ESP32的Wi-Fi和Web服務(wù)器能力將項目復(fù)雜度提升了一個層次但也更有趣。7. 調(diào)試技巧與常見問題排查在開發(fā)過程中你一定會遇到各種奇怪的問題。以下是我總結(jié)的一些排查思路屏幕一片空白首先檢查M5.begin()的參數(shù)是否正確確保LCD初始化被啟用。檢查屏幕連接是否牢固雖然Core2是一體的但如果是自己焊接的版本需注意。使用M5.Lcd.fillScreen(TFT_RED);等簡單測試代碼確認(rèn)屏幕驅(qū)動是否正常工作。確認(rèn)使用的M5Core2庫版本與你的硬件匹配。觸摸位置不準(zhǔn)首先確認(rèn)屏幕旋轉(zhuǎn)方向setRotation與你的坐標(biāo)計算是否匹配。旋轉(zhuǎn)后觸摸坐標(biāo)的坐標(biāo)系也會跟著旋轉(zhuǎn)。使用Serial.printf(Touch: (%d, %d)\n, M5.Touch.getX(), M5.Touch.getY());將原始觸摸坐標(biāo)打印出來觀察其范圍是否在0-319和0-239之間。如果不對可能是觸摸屏初始化校準(zhǔn)問題。M5Core2庫通常內(nèi)置了校準(zhǔn)但極端情況下可能需要手動校準(zhǔn)。計算邏輯錯誤在Serial中打印出每一步的狀態(tài)當(dāng)前操作數(shù)、上一個操作數(shù)、待執(zhí)行操作、isNewOperand標(biāo)志等。這是調(diào)試狀態(tài)機最有效的方法。編寫一組單元測試用例用已知的輸入序列驗證輸出是否正確。程序運行不穩(wěn)定或重啟檢查堆棧溢出。ESP32的堆棧不大避免在函數(shù)內(nèi)定義過大的局部數(shù)組如大尺寸的位圖緩沖區(qū)。將其移到全局或使用heap_caps_malloc從PSRAM分配如果板子支持。使用Serial.println(esp_get_free_heap_size());監(jiān)控內(nèi)存使用情況。檢查是否有遞歸函數(shù)調(diào)用過深。將Galdeano移植到M5Core2的過程是一次軟硬件結(jié)合的微型工程實踐。它要求你不僅理解計算器本身的邏輯還要駕馭嵌入式平臺的特性并思考如何創(chuàng)造良好的用戶體驗。最終當(dāng)你的指尖在屏幕上按下那個復(fù)古風(fēng)格的按鍵機器發(fā)出清脆的反饋并顯示出正確結(jié)果時那種成就感遠(yuǎn)超僅僅運行一個現(xiàn)成的程序。這個項目就像一個數(shù)字時代的“手工藝品”它不追求功能的繁多而在于對細(xì)節(jié)的打磨和對一種特定體驗的完整復(fù)現(xiàn)。你可以在此基礎(chǔ)上繼續(xù)深化比如為它設(shè)計一個3D打印的復(fù)古外殼或者將它改造成一個桌面擺件讓它從開發(fā)板變成一個真正的產(chǎn)品。