1. 开箱即用认识你的星火1号与RT-Thread如果你刚拿到星火1号开发板看着上面密密麻麻的元器件和接口可能会有点发怵。别担心我第一次接触它的时候也是这种感觉。但我要告诉你RT-Thread和星火1号的组合绝对是嵌入式入门到进阶的“黄金搭档”。星火1号是一块基于国产高性能MCU的开发板它集成了WiFi、蓝牙、丰富的GPIO和外设接口硬件底子非常扎实。而RT-Thread呢你可以把它理解为一个专门为这类资源有限的嵌入式设备设计的“微型操作系统”它把很多复杂的底层操作都封装好了提供了清晰的API让我们开发者可以更专注于业务逻辑而不是整天和寄存器、时钟树较劲。简单来说RT-Thread就像是你手机里的安卓或iOS系统它管理着硬件资源CPU、内存、外设提供了文件系统、网络协议栈比如WiFi、MQTT、设备框架等一大堆现成的“轮子”。我们写程序就是在这些“轮子”的基础上去实现具体的功能比如点亮一个LED或者让设备联网。这套组合最大的好处就是开发效率高。以前用裸机开发要实现一个联网功能你得从驱动开始一点点写现在用RT-Thread可能几行代码就搞定了。在开始敲代码之前我们需要把环境搭建好。首先你需要安装RT-Thread Studio这是官方的集成开发环境IDE基于Eclipse用起来很顺手。安装过程就是一路“下一步”这里就不赘述了。安装完成后打开Studio我们需要创建一个针对星火1号的项目。在新建项目时选择“基于开发板”然后在搜索框里输入“星火1号”或者“spark”就能找到对应的BSP板级支持包。选中它给项目起个名字比如my_spark_project点击完成一个最基础的项目框架就生成了。这个生成的项目里已经包含了星火1号所有外设的驱动和初始化代码我们不需要自己从头写。接下来我们需要配置一下RT-Thread的内核和组件。点击项目资源管理器里的RT-Thread Settings文件会打开一个图形化的配置界面。这里就像是一个开关面板你需要什么功能就打开对应的开关。对于我们的学习路径我建议你先确保以下几个组件是开启的PIN设备驱动用于GPIO控制、UART设备驱动用于串口调试打印、WiFi框架和SAL套接字抽象层用于网络连接。配置好后记得点击保存Studio会自动帮你更新项目代码。最后用USB线连接星火1号和电脑在Studio里选择正确的串口和下载器点击“编译并下载”你的第一个RT-Thread程序就跑在板子上了打开串口终端你应该能看到RT-Thread的Logo和命令行提示符msh /这说明系统启动成功了。2. 点亮第一盏灯GPIO控制入门实战万事开头难但点亮一个LED绝对是嵌入式世界最友好的“Hello World”。它能给你最直观的反馈让你立刻感受到程序对物理世界的控制力。在星火1号上板载了RGB三色LED我们先用最简单的红色LED来开始。首先我们要找到这颗LED连接在哪个GPIO引脚上。查看星火1号的原理图或者开发板手册可以知道红色LEDLED_R连接在MCU的PF12引脚上。在RT-Thread中我们不需要直接去操作寄存器的地址它提供了一个非常方便的PIN设备驱动框架。第一步在代码中定义这个引脚。RT-Thread提供了一个宏GET_PIN(port, pin)来将端口和引脚号转换成一个统一的引脚编号。所以我们这样定义#define PIN_LED_R GET_PIN(F, 12) // PF12 对应红色LED第二步在程序初始化部分比如main函数里我们需要将这个引脚设置为输出模式。因为我们要控制LED亮灭是向这个引脚输出高电平或低电平。/* 设置LED引脚为输出模式 */ rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT);第三步就是控制它了。让LED闪烁其实就是循环地输出低电平点亮和高电平熄灭中间加上延时。这里注意星火1号的LED是低电平点亮的所以PIN_LOW是开灯PIN_HIGH是关灯。while (1) { /* 点亮LED */ rt_pin_write(PIN_LED_R, PIN_LOW); rt_thread_mdelay(500); // 延时500毫秒 /* 熄灭LED */ rt_pin_write(PIN_LED_R, PIN_HIGH); rt_thread_mdelay(500); // 再延时500毫秒 }把这段代码放到main函数里编译下载你就能看到红色LED以1秒的周期稳定闪烁了。rt_thread_mdelay()是RT-Thread提供的毫秒级延时函数它会让当前线程睡眠把CPU让给其他任务这是多线程系统里更友好的做法而不是傻等。掌握了单色LED我们来玩点花的——RGB LED混色。原理是一样的RGB LED内部有三个独立的发光芯片红、绿、蓝分别由三个引脚控制。在星火1号上通常是红PF12、绿PE13、蓝PF11。我们同时控制这三个引脚输出不同的电平组合就能混合出各种颜色。比如想要紫色就是红色和蓝色同时点亮低电平绿色熄灭高电平。#define PIN_LED_R GET_PIN(F, 12) #define PIN_LED_G GET_PIN(E, 13) #define PIN_LED_B GET_PIN(F, 11) rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT); rt_pin_mode(PIN_LED_G, PIN_MODE_OUTPUT); rt_pin_mode(PIN_LED_B, PIN_MODE_OUTPUT); // 显示红色 rt_pin_write(PIN_LED_R, PIN_LOW); rt_pin_write(PIN_LED_G, PIN_HIGH); rt_pin_write(PIN_LED_B, PIN_HIGH); rt_thread_mdelay(1000); // 显示绿色 rt_pin_write(PIN_LED_R, PIN_HIGH); rt_pin_write(PIN_LED_G, PIN_LOW); rt_pin_write(PIN_LED_B, PIN_HIGH); rt_thread_mdelay(1000); // 显示蓝色 rt_pin_write(PIN_LED_R, PIN_HIGH); rt_pin_write(PIN_LED_G, PIN_HIGH); rt_pin_write(PIN_LED_B, PIN_LOW); rt_thread_mdelay(1000); // 显示黄色红绿 rt_pin_write(PIN_LED_R, PIN_LOW); rt_pin_write(PIN_LED_G, PIN_LOW); rt_pin_write(PIN_LED_B, PIN_HIGH); rt_thread_mdelay(1000);你可以写一个循环遍历一个颜色数组就能做出呼吸灯、彩虹灯等炫酷的效果了。这就是GPIO输出的基本操作是不是很简单它构成了我们与硬件交互最基础、也是最核心的手段。3. 感知物理世界按键输入与外部中断只会输出还不够一个智能设备必须能感知外部世界。最常见的输入就是按键。星火1号上有多个用户按键比如KEY0。我们来实现按下KEY0红色LED亮松开则灭的功能。首先查原理图知道KEY0连接在PC0引脚并且按键按下时引脚会被拉低到低电平GND松开时被上拉到高电平。第一步定义按键引脚并设置为输入模式同时因为内部有上拉电阻我们设置为上拉输入这样引脚默认就是高电平。#define PIN_KEY0 GET_PIN(C, 0) // PC0: KEY0 rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); // 上拉输入模式第二步在主循环里不断读取这个引脚的状态。这里有一个非常重要的细节按键消抖。机械按键在按下和松开的瞬间金属触点会因为弹性产生一连串快速的抖动电平会在高低之间快速跳变几次如果不处理程序可能会误判为多次按下。最简单的消抖方法就是检测到低电平后延时几十毫秒再读一次如果还是低电平才确认是有效按下。while (1) { /* 读取按键KEY0的引脚状态 */ if (rt_pin_read(PIN_KEY0) PIN_LOW) // 第一次检测到低电平 { rt_thread_mdelay(50); // 延时50ms避开抖动期 if (rt_pin_read(PIN_KEY0) PIN_LOW) // 再次确认 { /* 按键已被按下输出log点亮LED灯 */ LOG_D(KEY0 pressed!); rt_pin_write(PIN_LED_R, PIN_LOW); } } else { /* 按键没被按下熄灭LED灯 */ rt_pin_write(PIN_LED_R, PIN_HIGH); } rt_thread_mdelay(10); // 循环间隔降低CPU占用 }这种方法叫做“轮询”程序需要不停地主动去查看按键状态。对于简单的应用没问题但如果系统任务很多总去轮询会浪费CPU资源。更高效的方式是使用外部中断。中断的意思是当引脚电平发生特定变化比如从高变低时硬件会主动打断CPU当前的工作转而去执行我们预先设定好的一个函数中断服务程序执行完再回来。这样CPU平时就不用管按键了。我们用WK_UP按键连接在PA0通常有外部下拉按下为高电平来控制一个蜂鸣器连接在PB8为例。首先初始化引脚按键为浮空输入因为外部有下拉蜂鸣器为输出。#define PIN_WK_UP GET_PIN(A, 0) #define PIN_BEEP GET_PIN(B, 8) rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT); rt_pin_mode(PIN_BEEP, PIN_MODE_OUTPUT);然后绑定中断回调函数。我们设定WK_UP按键为上升沿触发因为按下时从低变高。/* 绑定中断下降沿触发回调函数名为irq_callback传入参数为PIN_WK_UP */ rt_pin_attach_irq(PIN_WK_UP, PIN_IRQ_MODE_RISING, irq_callback, (void*)PIN_WK_UP); /* 使能中断 */ rt_pin_irq_enable(PIN_WK_UP, PIN_IRQ_ENABLE);接下来是关键写中断回调函数。这个函数要尽量快不要做复杂的操作或调用可能引起阻塞的API。通常只是设置一个标志位通知其他线程去处理。static volatile rt_bool_t beep_status RT_FALSE; // 定义一个全局状态标志 void irq_callback(void *args) { rt_uint32_t sign (rt_uint32_t) args; if (sign PIN_WK_UP) { beep_status !beep_status; // 翻转状态 rt_pin_write(PIN_BEEP, beep_status ? PIN_HIGH : PIN_LOW); // 根据状态控制蜂鸣器 LOG_D(WK_UP interrupt. Beep status toggled.); } }最后主循环可以什么都不用做或者去处理其他任务。当按下WK_UP键时蜂鸣器状态就会立即改变。中断机制极大地提高了系统的实时性和效率在需要快速响应的场景如旋转编码器、限位开关中必不可少。4. 连接万物WiFi联网与网络通信让设备“上网”是物联网开发的核心。星火1号板载了WiFi模块RT-Thread也提供了完善的WiFi框架和网络协议栈这让联网变得异常简单。整个过程可以概括为扫描WiFi - 连接热点 - 获取IP - 进行网络通信。首先我们需要在RT-Thread Settings中确认WiFi框架和SAL套接字抽象层组件已经开启并正确配置。然后在代码中我们主要使用rt_wlan_开头的这一套API。连接WiFi通常有两种方式命令连接和代码连接。对于调试用命令连接最快。在串口终端msh里输入wifi scan这条命令会让设备扫描周围的WiFi热点并列出它们的SSID网络名、信号强度和加密方式。找到你的热点后使用join命令连接wifi join your_ssid your_password如果连接成功你会看到系统自动获取到了IP地址用ifconfig命令可以查看详细的网络信息。这种方式适合快速测试。而在实际产品代码中我们需要用编程的方式实现自动连接。RT-Thread的WiFi管理器支持自动重连和配置保存。下面是一个典型的连接流程我加上了详细的注释#include rtthread.h #include wlan_mgnt.h #include wlan_prot.h #define WLAN_SSID 你的WiFi名称 #define WLAN_PASSWORD 你的WiFi密码 #define NET_READY_TIME_OUT (rt_tick_from_millisecond(15 * 1000)) // 等待15秒超时 static struct rt_semaphore net_ready; // 用于等待网络就绪的信号量 /* WiFi就绪的回调函数当成功获取IP后会被调用 */ static void wlan_ready_handler(int event, struct rt_wlan_buff *buff, void *parameter) { rt_sem_release(net_ready); // 释放信号量通知主线程网络已就绪 LOG_D(WiFi is ready (Got IP).); } /* WiFi断开连接的回调函数 */ static void wlan_station_disconnect_handler(int event, struct rt_wlan_buff *buff, void *parameter) { LOG_W(WiFi disconnected!); } int wifi_connect_sample(void) { rt_err_t result RT_EOK; struct rt_wlan_info info; /* 初始化一个信号量用于同步等待 */ rt_sem_init(net_ready, net_ready, 0, RT_IPC_FLAG_FIFO); /* 注册事件回调当WiFi就绪获得IP时调用wlan_ready_handler */ rt_wlan_register_event_handler(RT_WLAN_EVT_READY, wlan_ready_handler, RT_NULL); /* 注册事件回调当WiFi断开时调用wlan_station_disconnect_handler */ rt_wlan_register_event_handler(RT_WLAN_EVT_STA_DISCONNECTED, wlan_station_disconnect_handler, RT_NULL); LOG_D(Start to connect AP: %s ..., WLAN_SSID); /* 执行同步连接。这个函数会阻塞直到连接成功或失败 */ result rt_wlan_connect(WLAN_SSID, WLAN_PASSWORD); if (result RT_EOK) { LOG_D(WiFi connected to AP successfully.); /* 获取当前连接的热点信息并打印 */ rt_wlan_get_info(info); LOG_D(SSID: %s, info.ssid.val); LOG_D(BSSID: %02x:%02x:%02x:%02x:%02x:%02x, info.bssid[0], info.bssid[1], info.bssid[2], info.bssid[3], info.bssid[4], info.bssid[5]); LOG_D(RSSI: %d, info.rssi); /* 等待‘网络就绪’信号量超时时间15秒 */ result rt_sem_take(net_ready, NET_READY_TIME_OUT); if (result RT_EOK) { LOG_I(Network is ready! IP address obtained.); // 可以在这里执行需要网络的操作比如MQTT连接、HTTP请求等 msh_exec(ifconfig, rt_strlen(ifconfig)); // 执行ifconfig命令查看IP } else { LOG_E(Wait for IP address timeout!); } } else { LOG_E(Connect to AP failed!); } /* 清理资源注销事件处理器脱离信号量 */ rt_wlan_unregister_event_handler(RT_WLAN_EVT_READY); rt_wlan_unregister_event_handler(RT_WLAN_EVT_STA_DISCONNECTED); rt_sem_detach(net_ready); return result; }这段代码的逻辑很清晰注册回调 - 发起连接 - 等待IP获取成功 - 进行后续操作。其中RT_WLAN_EVT_READY事件非常关键它代表设备不仅连上了路由器还成功通过DHCP获取到了IP地址此时才能真正进行网络通信。在实际项目中你可能会把wifi_connect_sample()这个函数放在系统启动时自动执行或者由一个按键触发。5. 迈向物联网MQTT协议实战应用设备连上网之后就要考虑如何与云端或其他设备通信了。在物联网领域MQTT协议因其轻量、高效、适合不稳定网络的特点成为了事实上的标准协议。RT-Thread提供了mqttclient软件包让我们能非常方便地接入阿里云、腾讯云等物联网平台或者自己的MQTT服务器。首先我们需要通过RT-Thread的包管理器env工具或者Studio的软件包中心添加mqttclient软件包。添加后记得在RT-Thread Settings中使能它并配置MQTT相关的参数比如默认服务器地址、端口通常是1883、客户端ID等。当然这些也可以在代码里动态设置。假设我们要连接一个公共的MQTT测试服务器test.mosquitto.org并订阅一个主题来接收消息同时定时发布消息到另一个主题。下面是一个精简但完整的示例#include rtthread.h #include mqtt_client.h static void mqtt_sub_callback(void* client, message_data_t* msg) { // 当订阅的主题收到消息时这个函数被调用 LOG_I(Topic: %.*s, msg-topic_name-lenstring.len, msg-topic_name-lenstring.data); LOG_I(Payload: %.*s, msg-message-payloadlen, (char*)msg-message-payload); } static void mqtt_demo_thread_entry(void *parameter) { mqtt_client_t *client RT_NULL; mqtt_message_t msg; char *pub_topic spark1/status; char *sub_topic spark1/command; int count 0; // 1. 创建MQTT客户端实例 client mqtt_lease(); // 2. 配置MQTT连接参数 mqtt_set_host(client, test.mosquitto.org); // MQTT服务器地址 mqtt_set_port(client, 1883); // 端口 mqtt_set_client_id(client, rtthread_spark1); // 客户端ID需唯一 mqtt_set_user_name(client, ); // 用户名如有 mqtt_set_password(client, ); // 密码如有 mqtt_set_clean_session(client, 1); // 清除会话 // 3. 设置收到消息的回调函数 mqtt_set_inbuf_size(client, 1024); mqtt_set_outbuf_size(client, 1024); mqtt_connect(client); // 发起连接 // 等待连接建立简单处理实际应用应加超时和重试 while(!mqtt_is_connected(client)) { rt_thread_mdelay(100); } LOG_I(MQTT Connected!); // 4. 订阅主题 mqtt_subscribe(client, sub_topic, QOS0, mqtt_sub_callback); LOG_I(Subscribed to topic: %s, sub_topic); memset(msg, 0, sizeof(msg)); msg.qos QOS0; msg.payload (void *)hello mqtt from spark1; while (1) { // 5. 定时发布消息 char payload[50]; rt_snprintf(payload, sizeof(payload), {\count\:%d}, count); msg.payloadlen rt_strlen(payload); msg.payload payload; mqtt_publish(client, pub_topic, msg); LOG_D(Published: %s to %s, payload, pub_topic); // 6. 让出CPU同时保持MQTT心跳和消息处理 mqtt_sleep_ms(client, 5000); // 这个函数内部会处理网络数据 } } int mqtt_sample_start(void) { rt_thread_t tid; tid rt_thread_create(mqtt_demo, mqtt_demo_thread_entry, RT_NULL, 2048, 25, 10); if (tid ! RT_NULL) { rt_thread_startup(tid); LOG_I(MQTT demo thread started.); } return 0; } /* 导出到MSH命令行方便测试 */ MSH_CMD_EXPORT(mqtt_sample_start, start MQTT client sample);这段代码做了几件关键事情创建客户端、配置参数、连接服务器、订阅主题、设置消息到达回调、循环发布消息。mqtt_sleep_ms()函数很重要它会在延时的同时处理网络接收的数据包包括心跳包和订阅消息保持连接活跃。在实际项目中你需要根据云平台的要求设置正确的客户端ID、用户名、密码通常是Token以及订阅和发布的具体主题。通过MQTT你的星火1号就能轻松地和云端双向通信实现远程监控、控制、数据上报等复杂功能了。6. 进阶探索传感器与数据上云综合项目掌握了GPIO、中断、WiFi和MQTT你已经具备了开发一个完整物联网设备的基本能力。现在我们来把这些知识串起来做一个综合性的小项目环境监测终端。这个项目会读取AHT10温湿度传感器的数据通过WiFi上传到MQTT服务器同时用板载的LCD屏显示实时数据还可以通过按键切换显示模式。首先我们需要添加AHT10传感器和LCD显示的软件包。在RT-Thread Settings中找到peripheral libraries and drivers启用aht10软件包和LCD驱动通常位于device drivers-using TFT LCD drivers。软件包管理器会自动下载代码并集成到你的项目中。传感器读取部分相对独立。AHT10通过I2C总线通信RT-Thread的软件包已经封装好了初始化、读取温湿度的函数我们直接调用即可。#include sensor.h #include aht10.h #define AHT10_I2C_BUS_NAME i2c2 // 根据原理图星火1号AHT10接在I2C2上 static void read_sensor_thread_entry(void *parameter) { rt_device_t dev RT_NULL; float humidity, temperature; dev aht10_init(AHT10_I2C_BUS_NAME); if (dev RT_NULL) { LOG_E(AHT10 sensor initialize failed!); return; } LOG_I(AHT10 sensor initialize success!); while (1) { humidity aht10_read_humidity(dev); temperature aht10_read_temperature(dev); if (!isnan(humidity) !isnan(temperature)) { LOG_I(Temp: %.1fC, Humi: %.1f%%, temperature, humidity); // 这里可以将数据存入全局变量供显示和网络线程使用 } rt_thread_mdelay(2000); // 每2秒读取一次 } }接下来是LCD显示。我们需要初始化LCD并编写一个显示函数将温湿度数据以美观的格式刷到屏幕上。RT-Thread的LCD驱动通常提供了一系列画点、画线、显示字符串的基础API。#include lcd.h static void lcd_display_task(float temp, float humi) { char str[32]; lcd_clear(WHITE); // 清屏为白色背景 lcd_set_color(BLACK, WHITE); lcd_show_string(10, 20, 24, Environment Monitor); lcd_set_color(BLUE, WHITE); rt_snprintf(str, sizeof(str), Temperature: %.1f C, temp); lcd_show_string(10, 60, 16, str); rt_snprintf(str, sizeof(str), Humidity: %.1f %%, humi); lcd_show_string(10, 90, 16, str); // 可以画个简单的图标或边框 lcd_draw_rectangle(5, 15, 235, 115); }最后我们需要一个主协调线程或者利用RT-Thread的FinSH线程将传感器数据、显示、网络上传和按键扫描整合起来。这里的关键是线程间通信。我们可以使用RT-Thread提供的邮箱、消息队列或者信号量让传感器读取线程将数据发送给显示和网络线程。为了简化这里用全局变量加互斥锁的方式示意static float current_temp 0.0f, current_humi 0.0f; static rt_mutex_t data_mutex RT_NULL; // 传感器线程更新数据 rt_mutex_take(data_mutex, RT_WAITING_FOREVER); current_temp temperature; current_humi humidity; rt_mutex_release(data_mutex); // 显示线程读取数据 rt_mutex_take(data_mutex, RT_WAITING_FOREVER); float disp_temp current_temp; float disp_humi current_humi; rt_mutex_release(data_mutex); lcd_display_task(disp_temp, disp_humi); // 网络线程读取并发布数据 // ... 组装JSON字符串通过MQTT发布按键可以用来切换LCD显示页面比如一页显示实时数据一页显示历史曲线如果存储了数据一页显示网络状态。通过这个综合项目你不仅复习了之前学到的所有知识点更关键的是学会了如何在一个多线程的实时操作系统中规划任务、管理资源、协调各个模块。这才是嵌入式RTOS开发的精髓所在。当你看到温湿度数据稳定地显示在屏幕上并同步更新到手机APP时那种成就感是无可比拟的。