风洞悬浮球:基于RT-Thread与MCXA156的简单控制实践 | 技术集结

描述

目录


 

项目概述


 

硬件选型与连接


 

软件架构设计


 

VL53L0X配置


 

风扇控制 (PWM)


 

控制算法


 

远程监控


 

OLED数据显示


 

有待改进的地方


 

项目源码


 

直播回放

1 项目概述

本项目基于 RT-Thread实时操作系统  NXP FRDM-MCXA156 开发板,构建了一个能够将乒乓球稳定悬浮在预定高度的控制系统。配备了本地数据显示屏和远程Web监控界面,构成了一个功能相对完整的嵌入式系统。

项目核心功能:

高度测量: 通过 VL53L0X ToF激光测距传感器,系统能够以毫米级精度实时获取乒乓球的高度。

复合控制算法: 考虑到管道内风的非线性特性,我没有采用单一的PID控制器,而是使用简单的 PID + 前馈 + 增益调度 的复合控制策略,以适应不同目标高度下的系统动态变化。

多任务并发处理: 基于RT-Thread的多线程架构,将核心控制、OLED显示、网络通信等任务分离到不同的线程中,确保了控制任务的实时性不受其他功能影响。

远程监控与人机交互: 系统通过 RW007 Wi-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 */    return RT_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_t tof_dev = rt_device_find("tof_vl53l0x");rt_device_open(tof_dev, RT_DEVICE_FLAG_RDONLY);// 2. 在主循环中读取数据structrt_sensor_datasensor_data;rt_size_t res = 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 */    return RT_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_t cfg, float speed){    // ... 省略参数检查 ...    // 核心逻辑:根据速度百分比计算脉冲宽度值    cfg->pulse = (int)(cfg->period * speed);    // 调用RT-Thread底层PWM API进行设置    rt_pwm_set(cfg->name, cfg->channel, cfg->period, cfg->pulse);    return RT_EOK;}

在 main.c 的主控制循环中,只需调用这个高层API即可:

  •  
  •  
  •  
  •  
  •  

/* --- main.c --- */// ... PID计算之后 ...float final_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 --- */// 每一次变化的步长#define RAMP_STEP 0.5f// 最终目标值float target_height = 250.0f;// PID控制器当前追踪的、平滑变化的目标float ramped_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 控制器核心计算float error = 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: 核心逻辑伪代码 ---# 任务一:定期请求状态async def request_status_periodically():    while True:        tcp_writer.write(b"get_status\r\n")        await asyncio.sleep(0.1)# 任务二:处理TCP收到的数据async def tcp_communication_manager():    while True:        message = await tcp_reader.read()        # 如果是JSON数据        ifis_json(message):            # 广播给所有WebSocket客户端            for client in clients:                await client.send(message)# 任务三:处理WebSocket客户端发来的命令async def handle_websocket_client(websocket):    async for command in websocket:        # 直接转发给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 --- */#ifndef SYSTEM_VARS_H#define SYSTEM_VARS_H// ...externint32_t current_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

 

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分