【读书笔记】usOS-II——互斥型信号量(mutex)

本文详细阐述了互斥信号量(mutex)的概念及其在解决优先级反转问题中的应用。介绍了互斥信号量如何通过优先级继承机制确保高优先级任务能够及时获取资源,避免资源竞争导致的任务延迟。

2008-11-23

互斥型信号量(mutex

互斥信号量功能:

1) 实现对资源的独占式访问(二值信号量)。

2) 降解优先级反转。

优先级反转:

使用实时内核,优先级反转问题是实时系统中出现得最多的问题。设,任务1优先级高于任务2,任务2优先级高于任务3。任务1和任务2处于挂起状态,等待某一事件的发生,任务3正在运行如[2.7(1)]。此时,任务3要使用其共享资源。使用共享资源之前,首先必须得到该资源的信号量(Semaphore)(2. 18.04信号量)。任务3得到了该信号量,并开始使用该共享资源[2.72]。由于任务1优先级高,它等待的事件到来之后剥夺了任务3CPU使用权[2.7(3)],任务1开始运行[2.7(4)]。运行过程中任务1也要使用那个任务3正在使用着的资源,由于该资源的信号量还被任务3占用着,任务1只能进入挂起状态,等待任务3释放该信号量[2.7(5)]。任务3得以继续运行[2.7(6)]。由于任务2的优先级高于任务3,当任务2等待的事件发生后,任务2剥夺了任务3CPU的使用权[2.7(7)]并开始运行。处理它该处理的事件[2.7(8)],直到处理完之后将CPU控制权还给任3[2.7(9)]。任务3接着运行[2.7(10)],直到释放那个共享资源的信号量[27(11)]。直到此时,由于实时内核知道有个高优先级的任务在等待这个信号量,内核做任务切换,使任务1得到该信号量并接着运行[2.7(12)]

在这种情况下,任务1优先级实际上降到了任务3 的优先级水平。因为任务1要等,直等到任务3释放占有的那个共享资源。由于任务2剥夺任务3CPU使用权,使任务1的状况更加恶化,任务2使任务1增加了额外的延迟时间。任务1和任务2的优先级发生了反转。

纠正的方法可以是,在任务3使用共享资源时,提升任务3的优先级。任务完成时予以恢复。任务3的优先级必须升至最高,高于允许使用该资源的任何任务。多任务内核应允许动态改变任务的优先级以避免发生优先级反转现象。然而改变任务的优先级是很花时间的。如果任务3并没有先被任务1剥夺CPU使用权,又被任务2抢走了CPU使用权,花很多时间在共享资源使用前提升任务3的优先级,然后又在资源使用后花时间恢复任务3的优先级,则无形中浪费了很多CPU时间。真正需要的是,为防止发生优先级反转,内核能自动变换任务的优先级,这叫做优先级继承(Priority inheritance)

互斥信号量降解优先级反转的过程:

mutex已被低优先级的任务3占用。高优先级的任务1提出申请mutex(调用Pend())。在这种情况下:

1) Pend函数注意到高优先级的任务要用这个共享资源,于是将任务3的优先级升高至9(创建mutex时指定,比任何提出申请mutex的任务的优先级都要高),并强制任务调度(由于任务3的优先级升高至9,因此任务3执行),任务3继续使用共享资源。当共享资源使用完后,任务3调用Post函数,释放mutex

2) Post函数注意到原来占用这个mutex的任务的优先级是被太高的,于是将任务3的优先级恢复到原来水平。

3) Post还注意到有个高优先级的的任务(任务1)正在等待这个mutex,于是将mutex交给这个任务,并做任务切换,让任务1运行。

互斥信号量的组成:

l 一个标志,指示mutex是否可用(OS_MUTEX_AVAILABLE表示可用)。

l 一个优先级,即优先级继承优先级(PIP)。

l 一个等待mutex的任务列表。

创建一个互斥信号量:OSMutexCreate()

互斥信号量创建初的几个成员的值:

OS_EVENT

OSEventType

OS_EVENT_TYPE_MUTEX

OSEventCnt

PIP

OS_MUTEX_AVAILABLE

OSEventPtr

NULL

OSEventGrp

0

OSEventTbl

0

PIP是该函数的参数,指定优先级继承优先级。当发生优先级反转时,将占用该mutex的任务的优先级太高到PIP

等待(申请)一个互斥信号量:OSMutexPend()

关键代码剖析:

void OSMutexPend (OS_EVENT *pevent, INT16U timeout, INT8U *err)

{

//不得在中断中调用该函数

if (OSIntNesting > 0) {

*err = OS_ERR_PEND_ISR;

return;

}

OS_ENTER_CRITICAL();

//信号量可用

if ((INT8U)(pevent->OSEventCnt & OS_MUTEX_KEEP_LOWER_8)

== OS_MUTEX_AVAILABLE) {

pevent->OSEventCnt &= OS_MUTEX_KEEP_UPPER_8;

//将计数器低8为置成占用该mutex的任务(当前任务)的优先级。

pevent->OSEventCnt |= OSTCBCur->OSTCBPrio;

//mutex中保存占用信号量的任务:修改该mutexOSEventPtr ,使其指向当前任务

pevent->OSEventPtr = (void *)OSTCBCur;

OS_EXIT_CRITICAL();

//信号量可用,正常返回。

*err = OS_NO_ERR;

return;

}

//信号量不可用:即已被占用

//从该信号量中获得PIP

pip = (INT8U)(pevent->OSEventCnt >> 8);

//从该信号量中获得占用该信号量的任务的优先级。

mprio = (INT8U)(pevent->OSEventCnt & OS_MUTEX_KEEP_LOWER_8);

//从信号量中获得占用该信号量的任务

ptcb = (OS_TCB *)(pevent->OSEventPtr);

/*

如果原先占用该mutex的优先级比提出申请该mutex的任务的优先级低

mprio > OSTCBCur->OSTCBPrio),则提升原任务的优先级至PIP

*/

if (ptcb->OSTCBPrio != pip && mprio > OSTCBCur->OSTCBPrio) {

if ((OSRdyTbl[ptcb->OSTCBY] & ptcb->OSTCBBitX) != 0x00) {

if ((OSRdyTbl[ptcb->OSTCBY] &= ~ptcb->OSTCBBitX) == 0x00) {

OSRdyGrp &= ~ptcb->OSTCBBitY;

}

//若原任务已就绪,则将其从就绪表中删除,并置就绪标志rdy

rdy = TRUE;

} else {

rdy = FALSE;

}

//修改优先级,及相关参数

ptcb->OSTCBPrio = pip;

ptcb->OSTCBY = ptcb->OSTCBPrio >> 3;

ptcb->OSTCBBitY = OSMapTbl[ptcb->OSTCBY];

ptcb->OSTCBX = ptcb->OSTCBPrio & 0x07;

ptcb->OSTCBBitX = OSMapTbl[ptcb->OSTCBX];

//如果原任务是就绪的,则继续让新的优先级就绪

if (rdy == TRUE) {

OSRdyGrp |= ptcb->OSTCBBitY;

OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;

}

OSTCBPrioTbl[pip] = (OS_TCB *)ptcb;

}

//让提出申请的任务先等待(从就绪表中删除,如mutex的等待队列)………

OSTCBCur->OSTCBStat |= OS_STAT_MUTEX;

OSTCBCur->OSTCBDly = timeout;

OS_EventTaskWait(pevent);

OS_EXIT_CRITICAL();

//执行任务切换(如果原来低优先级的任务优先级被抬高了,则该任务将被执行)

OS_Sched();

OS_ENTER_CRITICAL();

//提出申请的任务被唤醒继续执行

if (OSTCBCur->OSTCBStat & OS_STAT_MUTEX) {

//1)由于等待超时被定时器唤醒

OS_EventTO(pevent);

OS_EXIT_CRITICAL();

*err = OS_TIMEOUT;

return;

}

/*

2)原先占用mutex的任务执行完成释放了mutex

并唤醒了等待该mutex的最高优先级的任务

*/

OSTCBCur->OSTCBEventPtr = (OS_EVENT *)0;

OS_EXIT_CRITICAL();

*err = OS_NO_ERR;

}

释放一个互斥信号量:OSMutexPost()

关键代码剖析:

INT8U OSMutexPost (OS_EVENT *pevent)

{

//不得在中断中调用该函数

if (OSIntNesting > 0) {

return (OS_ERR_POST_ISR);

}

OS_ENTER_CRITICAL();

//从该信号量中获得PIP

pip = (INT8U)(pevent->OSEventCnt >> 8);

/*

从该信号量中获得占用该信号量的任务的优先级。

OSEventCnt中的低8位保存占用mutex的任务的原始优先级,

不随优先级的提高而改变。

*/

prio = (INT8U)(pevent->OSEventCnt & OS_MUTEX_KEEP_LOWER_8);

/*

确认释放mutex的任务确实是占用mutex的任务自身。

占用/申请mutex的任务的优先级可能是pip(被提高),也可能是原先任务的优先级。

*/

if (OSTCBCur->OSTCBPrio != pip &&

OSTCBCur->OSTCBPrio != prio) {

OS_EXIT_CRITICAL();

//若释放mutex的任务非占用/申请的任务,则返回错误信息。

return (OS_ERR_NOT_MUTEX_OWNER);

}

//若当前释放mutex的任务的优先级为pip,则需将该任务的优先级降到原来水平

if (OSTCBCur->OSTCBPrio == pip) {

//首先将pip从就绪表删除

if ((OSRdyTbl[OSTCBCur->OSTCBY] &= ~OSTCBCur->OSTCBBitX) == 0) {

OSRdyGrp &= ~OSTCBCur->OSTCBBitY;

}

//将任务优先级修改为原始优先级,并修改相关参数

OSTCBCur->OSTCBPrio = prio;

OSTCBCur->OSTCBY = prio >> 3;

OSTCBCur->OSTCBBitY = OSMapTbl[OSTCBCur->OSTCBY];

OSTCBCur->OSTCBX = prio & 0x07;

OSTCBCur->OSTCBBitX = OSMapTbl[OSTCBCur->OSTCBX];

//将修改优先级后的任务重新如就绪表

OSRdyGrp |= OSTCBCur->OSTCBBitY;

OSRdyTbl[OSTCBCur->OSTCBY] |= OSTCBCur->OSTCBBitX;

OSTCBPrioTbl[prio] = (OS_TCB *)OSTCBCur;

}

OSTCBPrioTbl[pip] = (OS_TCB *)1;

//mutex的等待列表不空,唤醒等待列表中最高优先级的任务,并将mutex分配给它

if (pevent->OSEventGrp != 0x00) {

/*

唤醒等待列表中最高优先级的任务(从mutex等待列表中删除,使其入就绪表),

清除等待任务的OS_STAT_MUTEX标志,并返回其优先级prio

*/

prio = OS_EventTaskRdy(pevent, (void *)0, OS_STAT_MUTEX);

pevent->OSEventCnt &= OS_MUTEX_KEEP_UPPER_8;

//mutex分配给新任务:置OSEventCntprio

pevent->OSEventCnt |= prio;

//mutex中保存占用信号量的任务

pevent->OSEventPtr = OSTCBPrioTbl[prio];

OS_EXIT_CRITICAL();

//任务切换(如果唤醒的任务优先级比当前任务高,则使唤醒的任务得到运行)

OS_Sched();

return (OS_NO_ERR);

}

//mutex的等待列表为空,即该mutex可用:置mutex可用标志及占用任务指针。

pevent->OSEventCnt |= OS_MUTEX_AVAILABLE;

pevent->OSEventPtr = (void *)0;

OS_EXIT_CRITICAL();

return (OS_NO_ERR);

}

/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2025 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "cmsis_os.h" #include <stdio.h> #include <string.h> /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ // DHT11引脚定义 #define DHT11_Pin GPIO_PIN_0 #define DHT11_GPIO_Port GPIOA // OLED I2C配置 #define OLED_I2C_ADDR 0x78 // OLED I2C地址,0x78或0x7A,根据实际模块调整 #define OLED_WIDTH 128 // OLED宽度像素 #define OLED_HEIGHT 64 // OLED高度像素 // 微秒延时函数精度校准值 #define DELAY_US_CALIB 5 // 根据实际时钟频率调整 /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ I2C_HandleTypeDef hi2c1; TIM_HandleTypeDef htim2; osThreadId defaultTaskHandle; /* USER CODE BEGIN PV */ // DHT11数据存储变量 uint8_t DHT11_Humi_Int = 0; // 湿度整数部分 uint8_t DHT11_Humi_Dec = 0; // 湿度小数部分 uint8_t DHT11_Temp_Int = 0; // 温度整数部分 uint8_t DHT11_Temp_Dec = 0; // 温度小数部分 uint8_t DHT11_CheckSum = 0; // 校验和 // OLED显示缓存 uint8_t OLED_GRAM[128][8]; /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_I2C1_Init(void); static void MX_TIM2_Init(void); void StartDefaultTask(void const * argument); /* USER CODE BEGIN PFP */ // DHT11相关函数 uint8_t DHT11_Read_Data(void); void DHT11_Set_Output(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void DHT11_Set_Input(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void HAL_Delay_us(uint32_t us); // OLED相关函数 void OLED_Init(void); void OLED_Clear(void); void OLED_Refresh_GRAM(void); void OLED_Write_Byte(uint8_t dat, uint8_t cmd); void OLED_Set_Pos(uint8_t x, uint8_t y); void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr); void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str); void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len); /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_I2C1_Init(); MX_TIM2_Init(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* USER CODE BEGIN RTOS_MUTEX */ /* add mutexes, ... */ /* USER CODE END RTOS_MUTEX */ /* USER CODE BEGIN RTOS_SEMAPHORES */ /* add semaphores, ... */ /* USER CODE END RTOS_SEMAPHORES */ /* USER CODE BEGIN RTOS_TIMERS */ /* start timers, add new ones, ... */ /* USER CODE END RTOS_TIMERS */ /* USER CODE BEGIN RTOS_QUEUES */ /* add queues, ... */ /* USER CODE END RTOS_QUEUES */ /* Create the thread(s) */ /* definition and creation of defaultTask */ osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 256); defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL); /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ /* USER CODE END RTOS_THREADS */ /* Start scheduler */ osKernelStart(); /* We should never get here as control is now taken by the scheduler */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) { Error_Handler(); } } /** * @brief I2C1 Initialization Function * @param None * @retval None */ static void MX_I2C1_Init(void) { /* USER CODE BEGIN I2C1_Init 0 */ /* USER CODE END I2C1_Init 0 */ /* USER CODE BEGIN I2C1_Init 1 */ /* USER CODE END I2C1_Init 1 */ hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN I2C1_Init 2 */ // 配置PB8(SCL)、PB9(SDA)为I2C复用功能 GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能GPIOB时钟 __HAL_RCC_GPIOB_CLK_ENABLE(); // PB8(SCL)配置:复用开漏、上拉 GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF_I2C1; // 注意:根据具体芯片号调整复用功能 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // PB9(SDA)配置 GPIO_InitStruct.Pin = GPIO_PIN_9; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); /* USER CODE END I2C1_Init 2 */ } /** * @brief TIM2 Initialization Function * @param None * @retval None */ static void MX_TIM2_Init(void) { /* USER CODE BEGIN TIM2_Init 0 */ /* USER CODE END TIM2_Init 0 */ TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; /* USER CODE BEGIN TIM2_Init 1 */ /* USER CODE END TIM2_Init 1 */ htim2.Instance = TIM2; htim2.Init.Prescaler = 71; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 999; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN TIM2_Init 2 */ /* USER CODE END TIM2_Init 2 */ } /** * @brief GPIO Initialization Function * @param None * @retval None */ static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; /* USER CODE BEGIN MX_GPIO_Init_1 */ /* USER CODE END MX_GPIO_Init_1 */ /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); /*Configure GPIO pin : DHT11_Pin */ GPIO_InitStruct.Pin = DHT11_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct); /* USER CODE BEGIN MX_GPIO_Init_2 */ /* USER CODE END MX_GPIO_Init_2 */ } /* USER CODE BEGIN 4 */ // DHT11相关函数实现 void HAL_Delay_us(uint32_t us) { uint32_t ticks = us * (SystemCoreClock / 1000000) / DELAY_US_CALIB; while (ticks--); } void DHT11_Set_Output(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOx, &GPIO_InitStruct); } void DHT11_Set_Input(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOx, &GPIO_InitStruct); } uint8_t DHT11_Read_Data(void) { uint8_t buf[5] = {0}; uint8_t i = 0, j = 0; // 发送起始信号 DHT11_Set_Output(DHT11_GPIO_Port, DHT11_Pin); HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); HAL_Delay_us(30); // 检测DHT11应答 DHT11_Set_Input(DHT11_GPIO_Port, DHT11_Pin); if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) { // 等待应答拉低结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); // 等待应答拉高结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); // 读取5字节数据 for (i = 0; i < 5; i++) { while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); HAL_Delay_us(40); for (j = 0; j < 8; j++) { while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); HAL_Delay_us(20); buf[i] <<= 1; if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { buf[i] |= 0x01; } while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); } } // 校验数据 if (buf[0] + buf[1] + buf[2] + buf[3] == buf[4]) { DHT11_Humi_Int = buf[0]; DHT11_Humi_Dec = buf[1]; DHT11_Temp_Int = buf[2]; DHT11_Temp_Dec = buf[3]; DHT11_CheckSum = buf[4]; return 0; // 读取成功 } else { return 1; // 校验失败 } } else { return 1; // 无应答 } } // OLED相关函数实现 void OLED_Write_Byte(uint8_t dat, uint8_t cmd) { uint8_t buf[2]; if(cmd) { buf[0] = 0x40; // 数据 } else { buf[0] = 0x00; // 命令 } buf[1] = dat; HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR << 1, buf, 2, 100); } void OLED_Set_Pos(uint8_t x, uint8_t y) { OLED_Write_Byte(0xb0 + y, 0); OLED_Write_Byte(((x & 0xf0) >> 4) | 0x10, 0); OLED_Write_Byte(x & 0x0f, 0); } void OLED_Clear(void) { uint8_t i, n; for(i = 0; i < 8; i++) { OLED_Write_Byte(0xb0 + i, 0); OLED_Write_Byte(0x00, 0); OLED_Write_Byte(0x10, 0); for(n = 0; n < 128; n++) OLED_Write_Byte(0, 1); } } void OLED_Init(void) { HAL_Delay(100); OLED_Write_Byte(0xAE, 0); // 关闭显示 OLED_Write_Byte(0x00, 0); // 设置低列地址 OLED_Write_Byte(0x10, 0); // 设置高列地址 OLED_Write_Byte(0x40, 0); // 设置起始行地址 OLED_Write_Byte(0xB0, 0); // 设置页地址 OLED_Write_Byte(0x81, 0); // 对比度设置,可调整亮度 OLED_Write_Byte(0xFF, 0); // 1~255 OLED_Write_Byte(0xA1, 0); // 设置段重定向 OLED_Write_Byte(0xA6, 0); // 正常显示 OLED_Write_Byte(0xA8, 0); // 设置多路复用率 OLED_Write_Byte(0x3F, 0); // 1/64 OLED_Write_Byte(0xC8, 0); // 重定向扫描方向 OLED_Write_Byte(0xD3, 0); // 设置显示偏移 OLED_Write_Byte(0x00, 0); OLED_Write_Byte(0xD5, 0); // 设置震荡器分频 OLED_Write_Byte(0x80, 0); OLED_Write_Byte(0xD9, 0); // 设置预充电周期 OLED_Write_Byte(0xF1, 0); OLED_Write_Byte(0xDA, 0); // 设置COM硬件引脚配置 OLED_Write_Byte(0x12, 0); OLED_Write_Byte(0xDB, 0); // 设置VCOMH取消选择级别 OLED_Write_Byte(0x40, 0); OLED_Write_Byte(0x8D, 0); // 电荷泵设置 OLED_Write_Byte(0x14, 0); // 启用电荷泵 OLED_Write_Byte(0xAF, 0); // 开启显示 OLED_Clear(); // 清屏 } // 显示一个字符 void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr) { uint8_t c = 0, i = 0; c = chr - ' '; // 获取偏移后的值 if(x > OLED_WIDTH - 1) { x = 0; y++; } OLED_Set_Pos(x, y); for(i = 0; i < 6; i++) OLED_Write_Byte(F6x8[c][i], 1); } // 显示字符串 void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str) { uint8_t j = 0; while (str[j] != '\0') { OLED_ShowChar(x, y, str[j]); x += 6; if(x > 122) { x = 0; y++; } j++; } } // 显示数字 void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len) { uint8_t t, temp; uint8_t enshow = 0; for(t = 0; t < len; t++) { temp = (num / __pow(10, len - t - 1)) % 10; if(enshow == 0 && t < len - 1) { if(temp == 0) { OLED_ShowChar(x + t * 6, y, ' '); continue; } else enshow = 1; } OLED_ShowChar(x + t * 6, y, temp + '0'); } } // 字符字模表 (6x8) const uint8_t F6x8[][6] = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 空格 {0x00, 0x00, 0x2f, 0x00, 0x00, 0x00}, // ! {0x00, 0x07, 0x00, 0x07, 0x00, 0x00}, // " {0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00}, // # {0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00}, // $ {0x62, 0x64, 0x08, 0x13, 0x23, 0x00}, // % {0x36, 0x49, 0x55, 0x22, 0x50, 0x00}, // & {0x00, 0x05, 0x03, 0x00, 0x00, 0x00}, // ' {0x00, 0x1c, 0x22, 0x41, 0x00, 0x00}, // ( {0x00, 0x41, 0x22, 0x1c, 0x00, 0x00}, // ) {0x14, 0x08, 0x3e, 0x08, 0x14, 0x00}, // * {0x08, 0x08, 0x3e, 0x08, 0x08, 0x00}, // + {0x00, 0x50, 0x30, 0x00, 0x00, 0x00}, // , {0x08, 0x08, 0x08, 0x08, 0x08, 0x00}, // - {0x00, 0x60, 0x60, 0x00, 0x00, 0x00}, // . {0x20, 0x10, 0x08, 0x04, 0x02, 0x00}, // / {0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00}, // 0 {0x00, 0x42, 0x7f, 0x40, 0x00, 0x00}, // 1 {0x42, 0x61, 0x51, 0x49, 0x46, 0x00}, // 2 {0x21, 0x41, 0x45, 0x4b, 0x31, 0x00}, // 3 {0x18, 0x14, 0x12, 0x7f, 0x10, 0x00}, // 4 {0x27, 0x45, 0x45, 0x45, 0x39, 0x00}, // 5 {0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00}, // 6 {0x01, 0x71, 0x09, 0x05, 0x03, 0x00}, // 7 {0x36, 0x49, 0x49, 0x49, 0x36, 0x00}, // 8 {0x06, 0x49, 0x49, 0x29, 0x1e, 0x00}, // 9 {0x00, 0x36, 0x36, 0x00, 0x00, 0x00}, // : {0x00, 0x56, 0x36, 0x00, 0x00, 0x00}, // ; {0x00, 0x08, 0x14, 0x22, 0x41, 0x00}, // < {0x14, 0x14, 0x14, 0x14, 0x14, 0x00}, // = {0x00, 0x41, 0x22, 0x14, 0x08, 0x00}, // > {0x02, 0x01, 0x51, 0x09, 0x06, 0x00}, // ? {0x32, 0x49, 0x79, 0x41, 0x3e, 0x00}, // @ // 更多字符... }; /* USER CODE END 4 */ /* USER CODE BEGIN Header_StartDefaultTask */ /** * @brief Function implementing the defaultTask thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartDefaultTask */ void StartDefaultTask(void const * argument) { /* USER CODE BEGIN 5 */ char display_buf[32]; // 初始化OLED OLED_Init(); // 显示标题 OLED_ShowString(0, 0, (uint8_t*)"DHT11 Monitor"); /* 无限循环 */ for(;;) { // 读取DHT11数据 if(DHT11_Read_Data() == 0) { // 显示湿度 sprintf(display_buf, "Humi: %d.%d %%", DHT11_Humi_Int, DHT11_Humi_Dec); OLED_ShowString(0, 2, (uint8_t*)display_buf); // 显示温度 sprintf(display_buf, "Temp: %d.%d C", DHT11_Temp_Int, DHT11_Temp_Dec); OLED_ShowString(0, 4, (uint8_t*)display_buf); } else { // 显示错误信息 OLED_ShowString(0, 2, (uint8_t*)"Read Error!"); OLED_ShowString(0, 4, (uint8_t*)"Check Sensor"); } // 2秒更新一次数据 osDelay(2000); } /* USER CODE END 5 */ } /** * @brief Period elapsed callback in non blocking mode * @note This function is called when TIM4 interrupt took place, inside * HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment * a global variable "uwTick" used as application time base. * @param htim : TIM handle * @retval None */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { /* USER CODE BEGIN Callback 0 */ /* USER CODE END Callback 0 */ if (htim->Instance == TIM4) { HAL_IncTick(); } /* USER CODE BEGIN Callback 1 */ /* USER CODE END Callback 1 */ } /** * @brief This function is executed in case of error occurrence. * @retval None */ void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { // 错误处理:可以添加LED闪烁等提示 } /* USER CODE END Error_Handler_Debug */ } #ifdef USE_FULL_ASSERT /** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */ void assert_failed(uint8_t *file, uint32_t line) { /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ } #endif /* USE_FULL_ASSERT */ /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ 已经配置好了freertos,IIC引脚为PB8、PB9,DHT11引脚PA0,写关于测试传感器数据在oled屏上显示的代码
最新发布
08-27
<think>我们正在讨论的是:在STM32CubeMX配置好FreeRTOS后,之前用标准外设库(SPL)写的代码(特别是DHT11和OLED的驱动)能否直接用在HAL库环境中。根据之前的分析,不能直接使用,需要修改。现在用户提供了具体的代码片段(DHT11和OLED显示),并希望测试在HAL库+FreeRTOS环境下显示温湿度数据。 用户提供的引用内容: [^1]: DHT11温湿度传感器的基本介绍。 [^2]: 一段DHT11的驱动代码(基于标准外设库?),包括初始化和读取函数。 [^3]: 主循环中调用DHT11_read()并显示在OLED上。 用户当前需求:在HAL库+FreeRTOS环境下,测试DHT11温湿度传感器并在I2C OLED显示屏上显示数据。 问题分析: 1. 用户提供的DHT11代码(引用[2])看起来是标准外设库风格(例如直接操作引脚,没有使用HAL库的API)。 2. 在HAL库环境中,我们需要将DHT11的驱动改写为使用HAL库的GPIO和延时函数,同时考虑FreeRTOS的影响(如延时函数要用FreeRTOS的延时,中断处理等)。 3. OLED显示屏通常使用I2C或SPI接口,在HAL库中需要使用HAL_I2C或HAL_SPI的API来驱动。 解决步骤: 步骤1:使用STM32CubeMX配置工程 - 配置时钟、调试接口(如SYS中的Serial Wire)。 - 配置DHT11连接的GPIO(作为输出和输入,注意在CubeMX中配置为GPIO_Output和GPIO_Input)。 - 配置OLED使用的I2C接口(比如I2C1),并配置好引脚。 - 配置FreeRTOS,选择CMSIS_V1或V2(根据需求),可以默认创建一个任务(比如用于读取传感器和刷新显示)。 - 生成代码(选择MDK-ARM或STM32CubeIDE等)。 步骤2:修改DHT11驱动为HAL库版本 - 原标准库的GPIO操作(如DHT11_PinSet(1))要改为HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_SET)等。 - 原标准库的延时函数(如mdelay)要改为HAL_Delay(在非RTOS任务中)或者使用FreeRTOS的vTaskDelay(在RTOS任务中)。但是注意DHT11的时序要求严格,通常需要微秒级延时,而FreeRTOS的vTaskDelay最小延时单位是一个tick(通常1ms),所以微秒级延时还是要用HAL_Delay或者循环实现。但是HAL_Delay也是毫秒级,所以通常用循环计数来实现微秒延时(或者使用定时器)。 - 注意:在FreeRTOS中,如果使用HAL_Delay(它基于SysTick),而SysTick被FreeRTOS接管,那么HAL_Delay实际上会调用FreeRTOS的延时函数,所以不会影响任务调度。但是微秒级延时不能使用HAL_Delay,需要自己实现。 步骤3:修改DHT11驱动中的微秒级延时 - 在HAL库中,我们可以使用一个简单的循环来实现微秒延时(注意,这种方法不精确,且受系统时钟影响,但对于DHT11这种低速设备通常可行)。例如: ```c void DHT11_Delay_us(uint16_t us) { uint32_t start = DWT->CYCCNT; uint32_t us_count = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < us_count); } ``` 注意:使用前需要使能DWT(Data Watchpoint and Trace)计数器。在初始化时调用: ```c void DWT_Init(void) { if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } } ``` 步骤4:修改DHT11的读取函数以适应HAL的GPIO操作 - 原代码中的引脚配置(如DHT11_PinCfgAsOutput)要改为使用HAL_GPIO_Init来配置,但通常我们在CubeMX中已经配置好GPIO,所以只需要在代码中切换方向(输出和输入)。在HAL库中,切换GPIO方向可以通过修改GPIO的Mode实现,例如: ```c // 设置为输出 void DHT11_IO_Output(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); } // 设置为输入 void DHT11_IO_Input(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; // 根据DHT11要求,上拉 HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); } ``` 或者更简单的方法:在驱动中直接使用寄存器切换方向(不推荐,但效率高): ```c #define DHT11_IO_IN() {DHT11_GPIO_PORT->MODER &= ~(3<<(2*DHT11_PIN_NUM)); DHT11_GPIO_PORT->MODER |= 0<<(2*DHT11_PIN_NUM);} // 输入模式 #define DHT11_IO_OUT() {DHT11_GPIO_PORT->MODER &= ~(3<<(2*DHT11_PIN_NUM)); DHT11_GPIO_PORT->MODER |= 1<<(2*DHT11_PIN_NUM);} // 输出模式 ``` 步骤5:修改OLED驱动为HAL库版本 - 如果原来的OLED驱动使用的是标准外设库的I2C函数,则需要改为HAL_I2C的函数。 - 例如,写一个字节到OLED: ```c void OLED_WriteByte(uint8_t data) { // 使用HAL_I2C_Mem_Write,或者HAL_I2C_Master_Transmit HAL_I2C_Mem_Write(&hi2c1, OLED_ADDRESS, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); } ``` 具体需要根据OLED的驱动芯片(如SSD1306)的指令格式来写。 步骤6:创建FreeRTOS任务 - 在CubeMX生成的FreeRTOS任务中(如默认任务或自定义任务),调用DHT11读取函数和OLED显示函数。 - 注意:读取DHT11需要一定时间(约4ms),如果任务优先级不高,可能会被其他任务打断,所以最好设置一个合适的优先级。另外,两次读取之间需要至少1秒的间隔(DHT11要求)。 步骤7:编写任务函数 - 例如: ```c void StartDefaultTask(void *argument) { int hum, temp; uint8_t dht11Data[5]; // 或者根据你的DHT11函数返回的数据结构 // 初始化DHT11(包括DWT初始化) DHT11_Init(); // 初始化OLED OLED_Init(); while(1) { if (DHT11_Read(&hum, &temp) == 0) // 假设修改后的DHT11_Read返回0表示成功 { // 在OLED上显示 OLED_ShowString(0, 0, "Hum: %d", hum); OLED_ShowString(0, 2, "Temp: %d", temp); } else { OLED_ShowString(0, 0, "DHT11 Error"); } vTaskDelay(2000); // 延时2秒,因为DHT11两次读取间隔至少1秒 } } ``` 注意事项: 1. DHT11的时序要求严格,在FreeRTOS任务中读取时,最好暂时关闭中断(或者提升任务优先级至最高)以避免被中断打断导致时序错误。但关闭中断会影响系统实时性,所以需要权衡。如果系统对实时性要求不高,可以关闭中断;否则,可以考虑使用信号量等机制来保护DHT11的读取过程(但要注意,DHT11的读取时间很短,约4ms)。 2. 如果使用关闭中断的方法,在读取DHT11前调用:`taskENTER_CRITICAL();`,读取完成后调用:`taskEXIT_CRITICAL();`。 总结:标准外设库的代码不能直接用于HAL库,需要将GPIO操作、延时函数、I2C/SPI通信等替换为HAL库的API,并考虑FreeRTOS的影响(如延时、任务调度、中断屏蔽等)。根据以上步骤修改后,即可在HAL库+FreeRTOS环境下运行。 如果用户有具体的代码片段需要帮助修改,可以提供。 相关问题: 1. 如何在STM32CubeMX中配置FreeRTOS和硬件外设(如I2C、GPIO)? 2. 如何实现微秒级延时(特别是在FreeRTOS环境下)? 3. 在FreeRTOS任务中读取传感器数据时,如何避免被其他任务或中断打断? 4. 如何将标准外设库的I2C驱动迁移到HAL库?</think>### 在STM32 HAL库+FreeRTOS环境下使用DHT11和OLED显示温湿度数据 #### 1. **核心迁移步骤** 在HAL库+FreeRTOS环境中,标准外设库代码需进行以下关键修改: | **组件** | **标准外设库实现** | **HAL库+FreeRTOS实现** | |----------------|----------------------------|-------------------------------------------| | **GPIO控制** | 直接寄存器操作 | `HAL_GPIO_WritePin()`/`HAL_GPIO_ReadPin()` | | **延时函数** | `mdelay()` | `osDelay()` (FreeRTOS) 或 `HAL_Delay()` | | **I2C通信** | 自定义I2C驱动 | `HAL_I2C_Master_Transmit()` | | **时序保证** | 阻塞延时 | FreeRTOS任务优先级控制 | #### 2. **DHT11驱动迁移(关键代码示例)** ```c // HAL库版本DHT11读取函数(需添加FreeRTOS保护) int DHT11_Read(int *hum, int *temp) { taskENTER_CRITICAL(); // 进入临界区保护时序[^2] HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); osDelay(18); // FreeRTOS延时18ms HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); // 使用HAL_GPIO_ReadPin替换直接寄存器操作 if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) != GPIO_PIN_RESET) { taskEXIT_CRITICAL(); return -1; // 超时错误 } // ...后续数据读取逻辑保持类似[^2]... taskEXIT_CRITICAL(); } ``` #### 3. **OLED驱动迁移(I2C示例)** ```c // HAL库版本OLED写入函数 void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; // 0x00表示命令 HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, buf, 2, 100); } // 显示温湿度任务 void DisplayTask(void *argument) { while(1) { int hum, temp; if(DHT11_Read(&hum, &temp) == 0) { char str[16]; sprintf(str, "H:%d%% T:%dC", hum, temp); OLED_ShowString(0, 0, str); // 使用HAL_I2C驱动[^3] } osDelay(2000); // 每2秒更新一次 } } ``` #### 4. **FreeRTOS集成关键点** 1. **任务创建**: ```c osThreadDef(displayTask, DisplayTask, osPriorityNormal, 0, 128); osThreadCreate(osThread(displayTask), NULL); ``` 2. **时序保护**: - DHT11的严格时序需使用`taskENTER_CRITICAL()`/`taskEXIT_CRITICAL()` - 或提升任务优先级至最高(如`osPriorityRealtime`) 3. **资源冲突避免**: - 若多任务访问I2C,需添加互斥锁: ```c osMutexDef(i2cMutex); osMutexId i2cMutexHandle = osMutexCreate(osMutex(i2cMutex)); void Safe_OLED_Write(uint8_t cmd) { osMutexWait(i2cMutexHandle, osWaitForever); HAL_I2C_Master_Transmit(...); osMutexRelease(i2cMutexHandle); } ``` #### 5. **调试建议** 1. **时序验证**: - 用逻辑分析仪检查DHT11的响应信号 - 确保起始信号低电平保持18±0.5ms 2. **错误处理增强**: ```c if (HAL_I2C_GetError(&hi2c1) != HAL_I2C_ERROR_NONE) { OLED_ShowString(0, 0, "I2C Error"); } ``` 3. **功耗优化**: - 在`osDelay()`期间自动进入低功耗模式: ```c void DisplayTask(void *arg) { while(1) { // ...读取显示代码... osDelay(2000); // 期间触发WFI睡眠 } } ``` > **迁移总结**:标准外设库代码需将GPIO/I2C操作替换为HAL API,延时函数改为FreeRTOS的`osDelay()`,严格时序部分需临界区保护。OLED显示逻辑可复用,但I2C底层需改用`HAL_I2C_Master_Transmit()`[^2][^3]。 ### 常见问题解决方案 | 问题现象 | 可能原因 | 解决方案 | |------------------------|--------------------------|------------------------------| | OLED无显示 | I2C地址错误 | 用`HAL_I2C_IsDeviceReady()`验证地址 | | DHT11读取超时 | 时序被中断打断 | 提升任务优先级或进临界区 | | 数据偶尔错误 | 校验失败 | 增加重试机制(最多3次) | | FreeRTOS卡死 | 堆栈溢出 | 增大任务栈大小(≥128字) | ### 相关问题 1. 如何在STM32CubeMX中配置FreeRTOS的堆栈大小? 2. HAL库中如何实现微秒级精确延时? 3. FreeRTOS任务间如何高效传递传感器数据? 4. STM32硬件I2C与软件模拟I2C在FreeRTOS下的性能对比? 5. 如何为DHT11驱动添加看门狗保护机制?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值