FreeRTOS高效应用实战
FreeRTOS高效应用实战
基于STM32CubeIDE生成对芯片移植好的FreeRTOS工程,使用HAL库编写FreeRTOS应用程序,实现FreeRTOS高效应用实战

引入函数句柄的概念
函数句柄(Function Handle)是编程中用于间接引用和操作函数的一种机制,其本质是将函数作为数据来传递和存储。以下是关于函数句柄的详细说明:
核心概念解析
特性 | 描述 |
---|---|
间接调用 | 通过变量调用函数,而非直接使用函数名 |
运行时绑定 | 允许在程序运行时动态确定要执行的函数 |
数据化函数 | 函数可像普通变量一样被赋值、传递和返回 |
在FreeRTOS中函数句柄大量使用,需对其有一定理解
任务
FreeRTOS 中的 任务(Task) 是系统调度的基本单元,类似于操作系统中的线程。每个任务代表一个独立的执行流程,拥有自己的堆栈空间和优先级
任务的核心特性
特性 | 说明 |
---|---|
独立性 | 每个任务拥有独立堆栈和程序计数器(PC) |
优先级驱动 | 系统基于优先级进行抢占式调度(0 最低,configMAX_PRIORITIES-1 最高) |
状态管理 | 任务在运行、就绪、阻塞、挂起状态间转换 |
资源隔离 | 任务通过信号量、队列等机制安全共享资源 |
低延迟切换 | FreeRTOS 任务切换时间通常在微秒级(取决于硬件) |
任务的五种状态
- 运行(Running):当前正在 CPU 上执行的任务
- 就绪(Ready):已准备好执行,等待调度器分配 CPU
- 阻塞(Blocked):因等待事件(如信号量、延迟)暂停执行
- 挂起(Suspended):被显式挂起,不参与调度(需手动恢复)
- 删除(Deleted):任务已终止,等待资源回收
任务调度机制
FreeRTOS 采用 抢占式调度 和 时间片轮转 的混合策略:
-
抢占规则
- 高优先级任务就绪时 立即抢占 低优先级任务
- 同优先级任务按 时间片轮转(默认为 1 个系统节拍)
-
调度触发条件
- 系统节拍中断(Tick Interrupt)
- 任务主动释放 CPU(
taskYIELD()
) - 资源释放(如发送信号量、队列)
声明任务头文件
#include "FreeRTOS.h"
#include "task.h"
声明任务句柄(x_Handle),定义任务属性(任务名称,堆栈大小,优先级)(x_attributes)
/* myTask_01_led1 的定义 */
osThreadId_t myTask_01_led1Handle; // 声明myTask_01_led1 的句柄
const osThreadAttr_t myTask_01_led1_attributes = { // myTask_01_led1 的属性
.name = "myTask_01_led1", // 任务名称
.stack_size = 128 * 4, // 堆栈大小为 512 字节
.priority = (osPriority_t) osPriorityLow, // 优先级为低
};
/* myTask02_led2_ 的定义 */
osThreadId_t myTask02_led2_Handle; // myTask02_led2_ 的句柄
const osThreadAttr_t myTask02_led2__attributes = { // myTask02_led2_ 的属性
.name = "myTask02_led2_", // 任务名称
.stack_size = 128 * 4, // 堆栈大小为 512 字节
.priority = (osPriority_t) osPriorityLow, // 优先级为低
};
任务执行函数(for死循环或while死循环)
/* USER CODE END Header_myTask01_led1 */
void myTask01_led1(void *argument)
{
/* USER CODE BEGIN myTask01_led1 */
/* Infinite loop */
for(;;)
{
/*用户编写执行程序
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1);
osDelay(200);
*/
}
/* USER CODE END myTask01_led1 */
}
创建任务
使用osThreadNew()函数创建任务,传入任务入口函数指针,任务入口函数参数指针(无则NULL),任务属性结构体指针
osThreadNew()返回值保存到任务句柄(x_Handle)
/* creation of myTask_01_led1 */
myTask_01_led1Handle = osThreadNew(myTask01_led1, NULL, &myTask_01_led1_attributes);
//任务句柄 // 任务函数 任务属性
/* creation of myTask02_led2_ */
myTask02_led2_Handle = osThreadNew(myTask02_led2, NULL, &myTask02_led2__attributes);
FreeRTOS初始化末尾创建任务,在初始化结束后,相同优先级任务按照时间片轮转执行,宏观则表现为任务同时执行
信号量
二值信号量:
二值信号量,可以理解为标志位
二值信号量(Binary Semaphore)在 FreeRTOS 中通常用于任务间的同步和互斥。
它的值只能是 0 或 1,适合用于实现简单的同步机制。以下是二值信号量的使用方法:
声明信号量头文件
#include "semphr.h"//操作信号量的头文件
声明二值信号量,创建二值信号量
SemaphoreHandle_t xBinarySemaphore;
xBinarySemaphore = xSemaphoreCreateBinary();
获取信号量
任务函数中:
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {//
// 成功获取信号量,执行相关操作
// ...
}
释放信号量
xSemaphoreGive(xBinarySemaphore);
使用示例
//1. **任务 A**:等待信号量
void vTaskA(void *pvParameters) {
while (1) {
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
// 处理任务
// ...
// 释放信号量,通知其他任务
xSemaphoreGive(xBinarySemaphore);
}
}
}
//2. **任务 B**:释放信号量
void vTaskB(void *pvParameters) {
while (1) {
// 执行某些操作
// ...
// 释放信号量,通知任务 A
xSemaphoreGive(xBinarySemaphore);
// 等待一段时间
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
注意事项
- 初始化: 二值信号量需要在使用前通过
xSemaphoreCreateBinary()
创建。- 首次释放: 在创建后,信号量默认是不可用的(值为0)。可以使用
xSemaphoreGive()
初始化为可用状态(值为1)。- 同步: 使用二值信号量可以确保任务的同步,比如在任务 A 中等待任务 B 释放信号量来继续执行。
这种信号量在实现任务间的简单同步时非常有用。
但可能出现优先级翻转问题
优先级翻转现象图解
TEXT任务优先级:Task_H(高) > Task_M(中) > Task_L(低)
时间轴 | 事件流
----------------------------------------
t0 Task_L 获取信号量
t1 Task_H 请求信号量 → 阻塞
t2 Task_M 就绪 → 抢占Task_L
t3 Task_M 长时间运行
t4 Task_L 无法继续执行 → 不能释放信号量
t5 Task_H 持续阻塞 → 系统实时性破坏
互斥信号量(Mutex)
优化方案:使用互斥信号量可以减少优先级翻转问题
解决办法:
使用互斥量替代二值信号量
定义互斥信号量句柄,属性
osMutexId_t tokenHandle;// 定义一个互斥锁句柄变量,用于后续操作互斥锁
const osMutexAttr_t token_attributes = {// 定义一个常量结构体,用于设置互斥锁的属性
// 设置互斥锁的名称为 "token",便于调试和识别
.name = "token"
};
创建互斥信号量
// 创建一个新的互斥锁,并返回其句柄
tokenHandle = osMutexNew(&token_attributes);
// tokenHandle 变量,用于存储互斥锁的句柄
// osMutexNew 函数,用于创建一个新的互斥锁
// &token_attributes 指向互斥锁属性的指针,用于配置互斥锁的行为
获取信号量,释放信号量
// 尝试获取信号量tokenHandle,等待时间为portMAX_DELAY(无限等待直到获取成功)
if (xSemaphoreTake(tokenHandle, portMAX_DELAY) == pdTRUE) //
{
USART1_printf("%s\r\n",strHigh);
HAL_Delay(10);
// 释放信号量tokenHandle
xSemaphoreGive(tokenHandle);
}
二值信号量与互斥量对比
特性 | 二值信号量 | 互斥量 |
---|---|---|
优先级继承 | 不支持 | 支持 |
递归获取 | 不可 | 可 |
初始状态 | 空 | 可用 |
适用场景 | 事件通知、简单同步 | 资源互斥访问 |
计数信号量
定义计数信号量
/* Definitions for Sem_Tables */
osSemaphoreId_t Sem_TablesHandle;// 假设 Sem_Tables_attributes 是预定义的信号量属性结构体
const osSemaphoreAttr_t Sem_Tables_attributes = {
.name = "Sem_Tables" // 设置信号量的名称
};
创建计数信号量
CountingSemHandle = osSemaphoreNew(5, 5, &CountingSem_attributes);
-
第一个参数
5
:信号量的最大计数值。 -
第二个参数
5
:信号量的初始计数值。 -
第三个参数
&CountingSem_attributes
:信号量的属性。 -
CountingSemHandle
:这是信号量的句柄,用于在代码中引用该信号量。
从计数信号量内获取一个信号量
xSemaphoreTake(CountingSemHandle, pdMS_TO_TICKS(100)//(计数信号量(剩余)数值减少)
释放计数信号量
xSemaphoreGiveFromISR(CountingSemHandle, &highTaskWoken);//(计数信号量(剩余)数值增加)
注意:_FromISR后缀函数是在中断服务程序(ISR)中运行的函数,确保中断安全
事件组(EvenGroup)
声明事件组头文件
#include "event_groups.h"
先设置事件组掩码(二进制),相当于标志位
#define BITMASK_KEY_LEFT (0b00000001<<2)//04 2事件位掩码
#define BITMASK_KEY_RIGHT (0b00000001<<0)//01 0
#define BITMASK_uart_bit (0b00000001<<1)//02 1
定义事件组句柄和属性。
eventGroupHandle = osEventFlagsNew(&eventGroup_attributes);//事件组句柄
/* Definitions for eventGroup */
osEventFlagsId_t eventGroupHandle;
const osEventFlagsAttr_t eventGroup_attributes = {
.name = "eventGroup"
};
设置事件组标志位
//设置事件组标志位,例如BITMASK_KEY_LEFT标志位
xEventGroupSetBits(eventGroupHandle, BITMASK_KEY_LEFT);//参数1:设置的事件组。参数2:设置的事件位
清除事件组掩码(事件组标志位)
//清除事件组掩码,相当于清除标志位
xEventGroupClearBits(eventGroupHandle, BITMASK_KEY_LEFT | BITMASK_KEY_RIGHT);
事件组运用——阻塞等待,事件组成立后执行用户程序
//以阻塞态等待事件组掩码
xEventGroupWaitBits(eventGroupHandle, BITMASK_KEY_LEFT | BITMASK_KEY_RIGHT,
pdTRUE, pdTRUE, portMAX_DELAY);//等待事件组中一个或多个事件位的设置,portMAX_DELAY处于阻塞状态一直等待
/*
* eventGroupHandle 是创建事件组时获得的句柄
BITMASK_KEY_LEFT | BITMASK_KEY_RIGHT 是要等待的事件位掩码,这表示等待 BITMASK_KEY_LEFT 和 BITMASK_KEY_RIGHT 事件位被设置。
pdTRUE 表示在返回时清除匹配的事件位。
pdTRUE 表示等待所有指定的事件位被设置。
xWaitForAllBits,
pdTRUE 等待所有目标位被设置
仅需等待其中任何一个目标位被设置(pdFALSE)
portMAX_DELAY 表示无限等待直到事件位被设置。
*/
//等待事件组条件成立退出等待,执行用户程序,例闪烁led
for (int i = 0; i < 10; ++i)
{
USART1_printf("keyleft+keydowm\r\n");
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2);
vTaskDelay(pdMS_TO_TICKS(500));
}
二进制数打印打印事件组事件位
uint8_t temp_str[20];
//USART1_printf("Current event bits = %02X\r\n", xEventGroupGetBits(eventGroupHandle));
//获取 eventGroupHandle 事件组中当前设置的事件位,并以十六进制格式输出到 USART1。%02X 格式说明符确保输出的值是两位的十六进制数。
printBinary(xEventGroupGetBits(eventGroupHandle));//将事件掩码以二进制数打印
总结:先定义事件(组)掩码(标志位),
设置事件掩码:按键按下后将对应事件位值设为1 #define BITMASK_KEY_LEFT (0b00000001<<2)//04事件位掩码
在任务中阻塞读取事件位/组,成功读取则执行对应事件/事件组 xEventGroupWaitBits(eventGroupHandle, BITMASK_KEY_LEFT | BITMASK_KEY_RIGHT,
清除事件位 xEventGroupClearBits(eventGroupHandle, BITMASK_KEY_LEFT | BITMASK_KEY_RIGHT);
多任务同步(EventSync)
基于事件组
在任务进入循环前,使用事件组等待函数,让函数等待事件组事件位成立才进入循环,运行任务。以此达到多任务同步
多任务同步,
等待各个任务的事件位都置1,同时开始各各任务
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, 事件组句柄,指定你要操作的事件组。
const EventBits_t uxBitsToSet, 在事件组中设置的事件标志位掩码。这些位将被设置为 1。
const EventBits_t uxBitsToWaitFor, 你希望等待的事件标志位掩码。函数会等待这些位被设置为 1。
TickType_t xTicksToWait )等待的时间,单位是 tick。可以使用 portMAX_DELAY 表示无限等待。
这个函数用于在 FreeRTOS 中同步事件组。它的参数说明如下:
xEventGroupSync(eventGroupHandle, BITMASK_KEY_RIGHT, BITMASK_SYNC, portMAX_DELAY);
这个 `xEventGroupSync` 函数用于同步 FreeRTOS 事件组。你传入的参数是:
- `eventGroupHandle`:事件组句柄。
- `BITMASK_KEY_RIGHT`:等待的事件标志位。
- `BITMASK_SYNC`:要设置的事件标志位。
- `portMAX_DELAY`:等待时间(无限等待)。
该程序同步点是三个按键都按下,
同步事件标志位为 按键一按下事件位|按键二按下事件位|按键三按下事件位
随意按下三个按键,同步事件开始,三个事件同时执行(多任务同步)
消息缓存区(MessageBuff)
消息缓存区是一种用于在任务之间传递数据的机制,它允许任务发送和接收不定长的消息。消息缓存区可以用于实现任务间的通信,而不需要预先知道消息的确切长度。
特点:
- 动态长度:消息缓存区可以处理任意长度的消息,这使得它非常适合于处理变长数据。
- 灵活的接口:消息缓存区提供了多种发送和接收函数,如
xMessageBufferSend
和xMessageBufferReceive
,可以根据需要选择合适的接口。- 阻塞和非阻塞操作:消息缓存区支持阻塞和非阻塞操作,可以根据任务的具体需求选择合适的操作模式。
- 内存管理:消息缓存区内部管理了所需的内存,用户不需要手动分配和释放内存。
消息缓存区的创建和使用通常包括以下步骤:
- 创建消息缓存区:使用
xMessageBufferCreate
函数创建一个消息缓存区,并返回一个MessageBufferHandle_t
类型的句柄。- 发送消息:使用
xMessageBufferSend
函数将消息发送到消息缓存区。该函数接受消息缓存区句柄、消息指针和消息长度作为参数。- 接收消息:使用
xMessageBufferReceive
函数从消息缓存区接收消息。该函数接受消息缓存区句柄、接收缓冲区指针、缓冲区长度和等待时间作为参数。
定义消息缓冲区句柄,创建一个消息缓存区
#define MSG_BUFFER_LEN 50//消息缓冲区大小
MessageBufferHandle_t msgBuffer;//创建消息缓冲区句句柄
msgBuffer = xMessageBufferCreate(MSG_BUFFER_LEN);//创建消息缓冲区
实现用消息缓存区发送接收字符串并打印
发送
uint8_t bytesCount = strlen(temp_str);//计算的字符串的长度,即字符串中字符的数量
if (msgBuffer != NULL) {//判断消息缓存区是否成功创建
uint16_t realCnt =//返回值是成功发送的字节数
xMessageBufferSend(msgBuffer,temp_str,bytesCount,portMAX_DELAY);//消息缓冲区对象句柄,待发送数据的指针,数据长度,等待时间
USART1_printf("Write bytes = %d\r\n", realCnt);//打印成功发的数据的字节数
}
接收
uint8_t receive_data[MSG_MAX_LEN];//接收缓存区
//接收消息缓存区数据
uint16_t realCnt = xMessageBufferReceive( msgBuffer, receive_data, MSG_MAX_LEN, portMAX_DELAY);//消息缓冲区句句柄,接收缓存区指针,接收消息缓存区的最大长度,等待时间
//realCnt:返回成功接收的数据长度
if (realCnt > 0) {
// 确保 receive_data 以 null 结尾,或打印字节数据
if (realCnt < MSG_MAX_LEN)
{
receive_data[realCnt] = '\0'; // 添加 null 终止符
USART1_printf("get Data = %s\r\n", receive_data); // 打印接收到的数据
}
USART1_printf("Read message bytes = %d\r\n", realCnt); // 打印接收到的字节数
}
任务通知
作用其一:模拟信号量
定义消息变量(32位)
uint32_t Notify_num=0;//创建要发送的任务通知变量(模拟消息队列)(只能是32位变量)
发布任务通知:
void Task_Send_Notify(void *argument)
{
/* USER CODE BEGIN Task_Send_Notify */
uint32_t Notify_num=0;//要发送的任务通知(模拟消息队列)
/* Infinite loop */
for(;;)
{
Notify_num++;
if (Task_2_Wait_GetHandle != NULL) //判断接收任务通知句柄是否存在
{
//BaseType_t highTaskWoken = pdFALSE;
xTaskNotify(Task_2_Wait_GetHandle, Notify_num, eSetValueWithOverwrite);
//任务通知,Task_ShowHandle为通知任务的句柄,
//Notify_num为通知的数据,
//通知操作类型。eSetValueWithOverwrite ,通知覆盖上一次通知值
// portYIELD_FROM_ISR(highTaskWoken);//用于在中断服务例程 (ISR) 结束时决定是否需要进行上下文切换的函数。
}
osDelay(200);
}
/* USER CODE END Task_Send_Notify */
}
此循环表示每200ms使变量Notify_num自增1,同时发布任务通知,通知接收任务(Task_2_Wait_GetHandle)
接收任务通知:
void Task_2_Wait_Get_Notify(void *argument)
{
/* USER CODE BEGIN Task_2_Wait_Get_Notify */
uint32_t pulNotification_Notify_num = 0;//同来接受通知值的变量
uint32_t ulBitsToClearOnEntry = 0x00000000;//进入时要清除的通知位掩码,设置为0表示不清除任何位。
uint32_t ulBitsToClearOnExit = 0xFFFFFFFF;//退出时要清除的通知位掩码,这里设置为F,表示在接收到通知后清除所有位。
/* Infinite loop */
for(;;)
{
//if (xTaskNotifyWait(ulBitsToClearOnEntry, ulBitsToClearOnExit,
// &pulNotificationValue, portMAX_DELAY) == pdPASS)
if (xTaskNotifyWait(0x00000000, 0xFFFFFFFF,&pulNotification_Notify_num, portMAX_DELAY) == pdPASS) //等待成功接收到通知,并将通知值写入pulNotificationValue变量
{
uint32_t Notify_num = pulNotification_Notify_num;//定义变量Notify_num接收任务通知传输的数据pulNotification_Notify_num
uint8_t temp_str[20];
USART1_printf("Notify_num= %d \r\n", Notify_num);
}
}
/* USER CODE END Task_2_Wait_Get_Notify */
}
if (xTaskNotifyWait(0x00000000, 0xFFFFFFFF,&pulNotification_Notify_num, portMAX_DELAY) == pdPASS)
//等待成功接收到通知,
pulNotification_Notify_num:存储任务通知的变量需要设置变量将任务通知的变量拷贝出来,防止覆盖
uint32_t Notify_num = pulNotification_Notify_num;
//定义变量Notify_num接收任务通知传输的数据pulNotification_Notify_num
作用其二:模拟计数信号量
任务通知————模拟计数信号量
作用:计数信号量为0时
ulTaskNotifyTake(pdFALSE,portMAX_DELAY)函数阻塞等待
用以控制任务的执行次数
xTaskNotifyGive(Task_2_Wait_GetHandle);
//向Task_2_Wait_GetHandle句柄的任务发送任务通知,任务通知值加1
//阻塞等待计数信号量
BaseType_t xClearCountOnExit = pdFALSE;//作计数信号量,退出时通知值减1
//BaseType_t xClearCountOnExit = pdTRUE;//作二值信号量,退出时通知值清0
ulTaskNotifyTake(xClearCountOnExit,portMAX_DELAY);//阻塞等待计数信号量增加为非0
消息队列
作用:用于任务间通信,交换变量值,相当于库函数全局变量
声明头文件
#include "queue.h" // 操作队列的头文件
声明句柄,设置消息队列属性(在此为命名)
osMessageQueueId_t myQueue_NUMHandle;
const osMessageQueueAttr_t myQueue_NUM_attributes = {
.name = "myQueue_NUM"
};
创建消息队列
myQueue_NUMHandle = osMessageQueueNew (16, sizeof(uint16_t), &myQueue_NUM_attributes);
写入消息队列
BaseType_t err = xQueueSendToBack(myQueue_NUMHandle, &num1, pdMS_TO_TICKS(50)); //将num1值写入消息队列myQueue_NUMHandle
if (err == errQUEUE_FULL)//判断是否写入队列,写入失败则是队列已满
{//写入失败
xQueueReset(myQueue_NUMHandle);//清空队列
}
接收消息队列
uint16_t num;//定义变量接消息队列信息
if (xQueueReceive(myQueue_NUMHandle, &num, pdMS_TO_TICKS(50)) != pdTRUE) {
continue; //用num变量接收myQueue_NUMHandle消息队列,判断是否接收成功,和等待时间
}//从队列中接收一条消息。如果接收失败(例如超时),则跳过当前循环的剩余部分并重新尝试接收。
USART1_printf("num = %d\r\n", num);//打印接收到的消息队列信息
打印消息队列属性
uint8_t queueName[30];
USART1_printf("Queue Name = %s\r\n", pcQueueGetName(myQueue_NUMHandle)); //打印消息队列名字
uint8_t queueSizeString[30];
USART1_printf("Queue size = %d\r\n", uxQueueSpacesAvailable(myQueue_NUMHandle)); //打印队列中当前等待的消息数量
软件定时器
分为周期定时器和单次定时器
周期定时器:每个定时周期结束执行一次回调函数
单次定时器:单个周期,周期结束执行单次回调函数
需要重启定时器才能重新开始定时周期
声明头文件
#include "timers.h"//软件定时器头文件
声明句柄,设置属性(在此为命名)
/* Definitions for Timer_Periodic */
osTimerId_t Timer_PeriodicHandle;
const osTimerAttr_t Timer_Periodic_attributes = {
.name = "Timer_Periodic"
};
/* Definitions for Timer_Once */
osTimerId_t Timer_OnceHandle;
const osTimerAttr_t Timer_Once_attributes = {
.name = "Timer_Once"
};
设置软件定时器周期
xTimerChangePeriod(Timer_PeriodicHandle, pdMS_TO_TICKS(1000), portMAX_DELAY);//设置周期定时器周期1000ms
xTimerChangePeriod(Timer_OnceHandle, pdMS_TO_TICKS(5000), portMAX_DELAY//设置单次定时器周期5000ms=5s
开启软件定时器
xTimerStart(Timer_PeriodicHandle, portMAX_DELAY);
xTimerStart(Timer_OnceHandle, portMAX_DELAY);
定时器回调函数
//周期定时器回调函数
void Callback_Timer_Periodic(void *argument)
{
/* USER CODE BEGIN Callback_Timer_Periodic */
counter++;
USART1_printf("Second=%d\r\n",counter);
if(counter%10==0)xTimerReset(Timer_OnceHandle, portMAX_DELAY);//重启单次定时器
/* USER CODE END Callback_Timer_Periodic */
}
//单次定时器回调函数
/* Callback_Timer_Once function */
void Callback_Timer_Once(void *argument)//5s后执行的单次定时器回调函数
{
/* USER CODE BEGIN Callback_Timer_Once */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2);
// HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET);//
/* USER CODE END Callback_Timer_Once */
}
操作定时器
判断单次定时器是否处于工作状态
if (xTimerIsTimerActive(Timer_OnceHandle) == pdFALSE) { }
重启单次定时器
xTimerReset(Timer_OnceHandle, portMAX_DELAY);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)