目錄
項目概述
硬件選型與連接
軟件架構設計
VL53L0X配置
風扇控制 (PWM)
控制算法
遠程監控
OLED數據顯示
有待改進的地方
項目源碼
直播回放
1 項目概述
本項目基于RT-Thread實時操作系統和NXP FRDM-MCXA156 開發板,構建了一個能夠將乒乓球穩定懸浮在預定高度的控制系統。配備了本地數據顯示屏和遠程Web監控界面,構成了一個功能相對完整的嵌入式系統。
項目核心功能:
高度測量: 通過VL53L0XToF激光測距傳感器,系統能夠以毫米級精度實時獲取乒乓球的高度。
復合控制算法: 考慮到管道內風的非線性特性,我沒有采用單一的PID控制器,而是使用簡單的PID + 前饋 + 增益調度的復合控制策略,以適應不同目標高度下的系統動態變化。
多任務并發處理: 基于RT-Thread的多線程架構,將核心控制、OLED顯示、網絡通信等任務分離到不同的線程中,確保了控制任務的實時性不受其他功能影響。
遠程監控與人機交互: 系統通過RW007Wi-Fi模塊接入局域網,并借助一個Python WebSocket代理,將數據實時推送到Web前端。用戶可以在瀏覽器上觀察高度、目標等參數的實時曲線,并可遠程下發指令,調整懸浮的目標高度。
本文將詳細剖析該系統的硬件選型、軟件架構、核心算法實現以及開發過程中遇到的挑戰與解決方案,希望能為對嵌入式控制系統感興趣的開發者提供一些參考和啟發。
2 硬件選型與連接
2.0 材料準備
管道:亞克力管子(外徑50mm 厚度2mm 內孔46mm 長50cm )
出風口罩:隨便找一個飲料瓶,剪掉上半,底部戳幾個洞,罩在管道一側,用于穩定管道末端風速并防止小球飛出
風扇:pwm 4線風扇(12v18000轉 4線)
降壓穩壓模塊:12v轉5v,要有12v輸出和5v輸出,注意共地
2.1 核心控制器:NXP FRDM-MCXA156
性能: 搭載 ARM Cortex-M33 內核,主頻高達 96MHz,為復雜的控制算法和多線程應用提供了充足的算力。
外設: 內置多個 I2C, SPI, PWM, UART 等接口,可以輕松連接本項目所需的各種外設,無需額外的擴展板。
生態: NXP官方提供了完善的SDK和文檔支持,同時RT-Thread也對其有良好的適配,大大降低了開發門檻。
2.2 各功能模塊
組件 | 型號 | 作用 | 選型理由 |
高度傳感器 | VL53L0X | ToF激光測距 | 提供毫米級、高頻率的距離測量,不受環境光干擾,是本項目精確控制的基礎。 |
執行器 (風扇) | YS4028B12H | 產生上升氣流 | 4028尺寸的暴力風扇,支持PWM調速,能夠提供足夠大的風量來托起乒乓球。 |
顯示屏 | SSD1306 | 128x64 OLED | I2C接口,功耗低,體積小,方便在本地實時顯示關鍵數據,便于調試。 |
Wi-Fi模塊 | RW007 | 網絡通信 | RT-Thread生態中常見的Wi-Fi模塊,驅動成熟,可以方便地讓設備接入網絡。 |
2.3 硬件連接詳情

所有模塊與 FRDM-MCXA156 開發板的連接如下表所示。
組件 | 開發板引腳 | 備注 |
PWM風扇 (YS4028B12H) | P3_6 | 連接到FLEXPWM0_A0,用于PWM調速 |
ToF傳感器 (VL53L0X) | P0_22(SCL),P0_23(SDA),P2_0(XSHUT) | 軟件I2C總線和開關控制 |
OLED屏幕 (SSD1306) | P1_9(SCL),P1_8(SDA) | 硬件I2C總線 (LPI2C2),配置參考SSD1306配置 |
Wi-Fi模塊 (RW007) | (SPI) | 連接到LPSPI1,配置參考rw007配置 |
調試串口 | P0_2(RX),P0_3(TX) | 連接到LPUART0 |
工作指示燈 | P3_12 | GPIO輸出 |
3 軟件架構設計
本項目的軟件核心是運行在MCXA156上的RT-Thread嵌入式固件,它與PC端的Python代理腳本和Web前端共同構成一個完整的監控系統。
3.1 整體架構圖
下圖展示了系統的三個主要部分(嵌入式設備、中間代理、用戶端)以及它們之間的數據流和交互關系。

3.2 工作流程詳解
數據采集與控制 (主控制線程): 系統核心,該線程以20ms的周期運行,在每個周期內:
通過I2C總線讀取VL53L0X傳感器的高度數據。
根據當前高度和斜坡化的目標高度,執行PID + 前饋 + 增益調度算法,計算出最終的風扇PWM占空比(轉速)。
調用PWM驅動,更新風扇轉速。
將當前高度、目標高度等狀態信息寫入全局變量,供其他線程使用。
本地顯示 (OLED顯示線程): 該線程獨立于主控制循環,它定期從全局變量中讀取最新的系統狀態,并將其格式化后顯示在SSD1306屏幕上。
遠程通信 (Websocket代理):
PC上運行的websocket_proxy.py腳本一方面通過TCP連接到設備的TCP服務器,另一方面啟動一個WebSocket服務等待Web瀏覽器的連接。代理腳本定期向設備發送get_status命令,設備執行該命令后,會返回一個包含所有狀態信息的JSON字符串。腳本解析此字符串,并通過WebSocket將其推送給前
4 VL53L0X配置
可以直接使用vl53l0x這個軟件包
4.1 軟件包的啟用與配置
啟用軟件包:在 RT-Thread online packages ---> peripheral libraries and drivers ---> sensors drivers ---> 中,勾選[*] VL53L0X Time of flight(TOF) sensor.。
sensor_v1驅動:勾選[*] Enable sensor_v1 divce framework 并啟用sample。
配置I2C總線:使用軟件I2C,配置如下

修改一下sample:
MSH_CMD_EXPORT(read_distance_sample, read distance sample);staticintrt_hw_vl53l0x_port(void){ structrt_sensor_configcfg; cfg.intf.dev_name="i2c1"; /* i2c bus */ cfg.intf.user_data= (void*)0x29; /* i2c slave addr */ rt_hw_vl53l0x_init("vl53l0x", &cfg,64);/* xshutdown ctrl pin */ returnRT_EOK;}INIT_COMPONENT_EXPORT(rt_hw_vl53l0x_port);// 在vl53l0x_sensor_v1.c中#define VL53L0X_I2C_BUS "i2c1" /* i2c linked */
4.2 應用層調用
配置完成后,VL53L0X傳感器就被注冊為RT-Thread系統中一個名為tof_vl53l0x的標準設備(要把RT_NAME_MAX改大一點才能顯示完全)。在main.c中,我們只需通過標準設備接口即可與之交互,代碼非常簡潔:
/* --- main.c --- */// 1. 查找并打開設備rt_device_ttof_dev =rt_device_find("tof_vl53l0x");rt_device_open(tof_dev, RT_DEVICE_FLAG_RDONLY);// 2. 在主循環中讀取數據structrt_sensor_datasensor_data;rt_size_tres =rt_device_read(tof_dev,0, &sensor_data,1);// 3. 獲取毫米為單位的距離值current_height = sensor_data.data.proximity;
5 風扇控制 (PWM)
參考Servo_sg90庫,改一改就能用,當然也可以直接操作pwm
5.1 引腳配置
MSH_CMD_EXPORT(read_distance_sample, read distance sample);staticintrt_hw_vl53l0x_port(void){ structrt_sensor_configcfg; cfg.intf.dev_name="i2c1"; /* i2c bus */ cfg.intf.user_data= (void*)0x29; /* i2c slave addr */ rt_hw_vl53l0x_init("vl53l0x", &cfg,64);/* xshutdown ctrl pin */ returnRT_EOK;}INIT_COMPONENT_EXPORT(rt_hw_vl53l0x_port);// 在vl53l0x_sensor_v1.c中#define VL53L0X_I2C_BUS "i2c1" /* i2c linked */
5.2 核心實現
驅動的核心在于ys4028b12h_set_speed函數:
/* --- applications/fan/YS4028B12H.c --- */rt_err_tys4028b12h_set_speed(ys4028b12h_cfg_tcfg,floatspeed){ // ... 省略參數檢查 ... // 核心邏輯:根據速度百分比計算脈沖寬度值 cfg->pulse = (int)(cfg->period * speed); // 調用RT-Thread底層PWM API進行設置 rt_pwm_set(cfg->name, cfg->channel, cfg->period, cfg->pulse); returnRT_EOK;}
在main.c的主控制循環中,只需調用這個高層API即可:
/* --- main.c --- */// ... PID計算之后 ...floatfinal_fan_speed = ff_speed + pid_output;// ... 限幅之后 ...ys4028b12h_set_speed(cfg, final_fan_speed);
便于維護和拓展
6 控制算法
考慮到風洞系統是一個典型的非線性系統(在不同高度,維持懸浮所需的風力變化并非線性),單一的PID控制器難以在整個高度范圍(50mm ~ 450mm)內都取得良好效果。因此,本項目采用了一套簡單的復合控制策略。
6.1 復合控制策略概覽
我們的控制算法主要由以下四個部分組成,它們在 main.c 的主控制循環中協同工作:
設定值斜坡 (Ramped Setpoint):變更高度時讓目標高度緩慢變化,避免目標高度突變帶來的系統劇烈震蕩。
增益調度 (Gain Scheduling):根據當前目標高度,動態調整PID參數(Kp, Ki, Kd),以適應系統的非線性。
前饋控制 (Feed-forward):引入一個基礎風速,作為PID控制的“預補償”,加快系統響應。
PID閉環控制:經典的比例-積分-微分控制器,用于消除系統的穩態誤差。
6.2 代碼實現
設定值斜坡
當用戶設置一個新的target_height時,不直接將其作為PID控制器的目標,而是引入一個中間變量ramped_height,讓它以固定的步長RAMP_STEP逐漸逼近最終目標。
/* --- main.c --- */// 每一次變化的步長#defineRAMP_STEP 0.5f// 最終目標值floattarget_height =250.0f;// PID控制器當前追蹤的、平滑變化的目標floatramped_height =250.0f;// 在主循環中if(FABS(ramped_height - target_height) > RAMP_STEP) { if(ramped_height < target_height) ramped_height += RAMP_STEP;? ? else?ramped_height -= RAMP_STEP;? ? // 當追蹤目標變化時,需要同步更新PID增益? ? update_pid_gains_by_target(ramped_height);}?else?{? ? ramped_height = target_height;}
這樣做可以有效防止因目標值瞬間變化過大而導致的超調和震蕩。
增益調度與前饋
增益調度和前饋都基于預先測定好的查找表。在main.c中,我們定義了gain_schedule_table 和 ff_table。
gain_schedule_table: 存儲了不同高度下,經過優化的PID參數組。
ff_table: 存儲了不同高度下,一個大致能讓球懸浮的基礎風速(事實上,很難找,找到一個緩慢上升的速度就可以了)。
update_pid_gains_by_target()和get_feedforward_speed()函數會根據當前的ramped_height,通過線性插值的方式,從查找表中計算出最合適的Kp, Ki, Kd和基礎風速ff_speed。
/* --- main.c: update_pid_gains_by_target() 偽代碼 --- */// 1. 找到目標高度所在的區間lower_bound = find_closest_lower_entry(target);upper_bound = find_closest_upper_entry(target);// 2. 計算插值比例ratio = (target - lower_bound.height) / (upper_bound.height - lower_bound.height);// 3. 線性插值計算出當前高度對應的Kp, Ki, KdKP = lower_bound.kp + ratio * (upper_bound.kp - lower_bound.kp);KI = lower_bound.ki + ratio * (upper_bound.ki - lower_bound.ki);KD = lower_bound.kd + ratio * (upper_bound.kd - lower_bound.kd);
PID核心計算與輸出
最后,將前饋控制量與PID控制器的輸出量相加,得到最終的風扇速度。
/* --- main.c: 主控制循環核心 --- */// PID 控制器核心計算floaterror= ramped_height - (float)current_height;integral_error +=error;float derivative_error =error- previous_error;float pid_output = (KP *error) + (KI * integral_error) + (KD * derivative_error);previous_error =error;// 前饋與PID輸出合并float ff_speed = get_feedforward_speed(ramped_height);float final_fan_speed = ff_speed + pid_output;// 輸出限幅與積分抗飽和if(final_fan_speed >1.0f) { final_fan_speed =1.0f; integral_error -=error;// 抗飽和:當輸出飽和時,減去本次積分量}if(final_fan_speed 0.0f) {? ? final_fan_speed =?0.0f;? ? integral_error -=?error;?// 抗飽和}// 設置風扇速度ys4028b12h_set_speed(cfg, final_fan_speed);
當計算出的final_fan_speed超出物理限制(>1.0或<0.0)時,我們會從積分項integral_error中減去(或加上)本次的誤差。這可以防止積分項在輸出飽和期間無限制地累積,從而避免了在系統恢復時可能出現的巨大超調。
7 遠程監控

為了讓控制過程可視化,并能遠程干預,本項目設計了一套基于Web的遠程監控系統。其核心思想是:在設備端運行一個自定義的TCP應用層協議服務器,并通過一個PC端的Python腳本作為代理,將TCP協議轉換成Web前端通用的WebSocket協議。
7.1 設備端:自定義TCP服務器 (remote.c)
參考network_samples,我在設備端實現了一個簡單的TCP服務器(applications/remote/remote.c),它監聽在5000端口。這個服務器的功能非常專一,只響應兩個核心命令:
get_status: 當收到此命令時,服務器會立即讀取系統中所有相關的全局變量(如當前高度、目標高度、PID參數等),將它們打包成一個JSON字符串,然后發送給客戶端。
pid_tune: 當收到此命令(例如pid_tune -t 300)時,服務器會直接調用已有的pid_tune()MSH函數,從而實現對系統參數的修改。
/* --- applications/remote/remote.c: 命令分發偽代碼 --- */// ... 接收并解析命令 ...if(strcmp(argv[0],"get_status")==0){// 打包JSON并發送sprintf(send_buf,"{"current_height":%ld, ... }", current_height,...); send(connected, send_buf,...);}elseif (strcmp(argv[0],"pid_tune")==0){// 直接調用MSH函數 pid_tune(argc, argv); send(connected,"OK\r\n",...);}
這里復用了為串口命令行調試而編寫的pid_tune函數,因為只需要執行修改不需要回顯,系統的實時信息由get_status完成。
7.2 中間代理:WebSocket協議轉換器 (websocket_proxy.py)
它使用Python的asyncio和websockets庫,同時維護兩類連接:
1.一個到設備TCP服務器的持久連接:
它會定期(每100ms)自動發送get_status命令來拉取最新數據。
當從TCP連接收到設備返回的JSON數據后,它會立即將這些數據廣播給所有連接到它的WebSocket客戶端。
多個來自Web瀏覽器的WebSocket連接:
當從任何一個WebSocket客戶端收到命令時(如用戶在網頁上調整了目標高度),它會將這條命令原封不動地通過TCP連接轉發給設備。
# --- applications/remote/websocket_proxy.py: 核心邏輯偽代碼 ---# 任務一:定期請求狀態asyncdefrequest_status_periodically(): whileTrue: tcp_writer.write(b"get_status\r\n") awaitasyncio.sleep(0.1)# 任務二:處理TCP收到的數據asyncdeftcp_communication_manager(): whileTrue: message =awaittcp_reader.read() # 如果是JSON數據 ifis_json(message): # 廣播給所有WebSocket客戶端 forclientinclients: awaitclient.send(message)# 任務三:處理WebSocket客戶端發來的命令asyncdefhandle_websocket_client(websocket): asyncforcommandinwebsocket: # 直接轉發給TCP服務器 tcp_writer.write(command.encode())
8 OLED數據顯示
除了遠程監控,本項目還集成了一塊 SSD1306 OLED屏幕,用于在設備端實時顯示關鍵信息。
8.1 顯示線程
為了不影響核心控制邏輯的實時性,要將OLED的刷新任務放在一個獨立的線程中。
/* --- main.c --- */screen_thread =rt_thread_create("ScreenUpdate", screen_on,RT_NULL,1280,11,20);rt_thread_startup(screen_thread);
screen_on函數(位于applications/OLED/screen.c)是這個線程的實體。它與主控制線程是并發執行的。
8.2 通過全局變量進行線程間通信
我創建了一個頭文件applications/system_vars.h,在其中使用 extern 關鍵字聲明了所有需要在線程間共享的變量。
/* --- applications/system_vars.h --- */#ifndefSYSTEM_VARS_H#defineSYSTEM_VARS_H// ...externint32_tcurrent_height;externfloat target_height;// ...#endif
主控制線程作為生產者,在每次循環的最后,會更新這些全局變量的值。
OLED顯示線程作為消費者,它只需包含system_vars.h這個頭文件,就可以直接訪問這些變量的最新值。
/* --- applications/OLED/screen.c --- */#include// 包含全局變量聲明voidscreen_on(){ // ... u8g2初始化 ... while(1) { // 直接讀取全局變量 sprintf(buf,"Current: %d", current_height); u8g2_DrawStr(&u8g2,10,18, buf); sprintf(buf,"Target: %d", (int)target_height); u8g2_DrawStr(&u8g2,10,36, buf); u8g2_SendBuffer(&u8g2); rt_thread_mdelay(500);// 降低刷新率,避免閃爍 }}
這種“生產者-消費者”模式,通過共享內存(全局變量)進行通信,是一種簡單高效的線程間數據交換方式。對于本項目這種數據關系簡單、實時性要求不極端的場景來說,是一個非常合適的選擇。
9 有待改進的地方
傳感器的I2C問題:VL53L0X傳感器在連接到MCXA156的硬件I2C總線時,始終無法被正確初始化,會卡在一個叫VL53L0X_device_read_strobe的函數上,嘗試次數超過2000,然后返回錯誤。排查了連線問題,也確認了I2C引腳配置了(SSD1306可以正常使用),找不出來,發現使用軟件i2c沒問題就擺了。
控制精度:因為控制算法比較簡單,而且PWM風扇對PWM占空比的響應也不是完全線性的(階梯式的),加之管道內的風流比較復雜,所以小球在懸浮時最大會有10mm左右的上下波動,無法做到讓小球精確的懸浮在指定的高度。
控制算法優化:目前的PID參數和前饋表是通過手動試湊和簡單的評估腳本(貝葉斯優化)得到的。未來可以引入更高級的系統辨識方法(如基于MATLAB的System Identification Toolbox)來建立更精確的數學模型,或采用在線自動整定算法(如Ziegler-Nichols或繼電反饋法)來自動優化參數。
增加魯棒性:可以增加對傳感器數據異常值的濾波處理(如卡爾曼濾波),以應對乒乓球快速晃動或偶爾的測量噪聲。
風扇驅動:暫時只寫了set_speed,還有通過pwm讀取當前轉速的功能沒有做。
希望本文的分享,能為同樣走在嵌入式開發道路上的朋友們提供一些有價值的參考。
10 項目源碼
GitHub倉庫地址:https://github.com/Cylopsis/Little-Wind-Tunnel
歡迎大家提出寶貴的意見和建議。
11 直播回放
微信視頻號直播講解回放:
RT-Thread Github 開源倉庫,歡迎撒個星(Star)支持,更期待你的代碼貢獻:https://github.com/RT-Thread/rt-thread
-
開發板
+關注
關注
25文章
6090瀏覽量
112293 -
實時操作系統
+關注
關注
1文章
206瀏覽量
31751 -
RT-Thread
+關注
關注
32文章
1527瀏覽量
44147
發布評論請先 登錄
RT-Thread編程指南
RT-Thread全球技術大會:RT-Thread開源重塑軟件發展新生態
RT-Thread全球技術大會:RT-Thread Smart更好的兼容Linux生態
RT-Thread全球技術大會:Kconfig在RT-Thread中的工作機制
免費申請 | FRDM-MCXA156評測活動發布!
《恩智浦FRDM-MCXA156開發實踐指南》上線啦
CherryUSB-HID設備實踐 | 技術集結
明晚!基于RT-Thread 的 PID 控制實踐,從驅動到算法到監控,一次學會!| 問學直播

風洞懸浮球:基于RT-Thread與MCXA156的簡單控制實踐 | 技術集結
評論