STM32之紅外遙控訊號自學習實現
一、序言
很早前就想實現這個紅外遙控自學習的這個實驗,用於來自己控制房子裡如空調等紅外遙控裝置的自動化,NEC的標準到具體的產品上可能就被廠家定義為不一樣了,所以自學習就應該是接收到什麼就傳送什麼,不用管內容是什麼!
二、硬體實現原理
由上述原理圖可知,當IE為高電平時傳送紅外光,為低電平時不傳送紅外光。
在NEC協議中,資訊傳輸是基於38K載波,也就是說紅外線是以載波的方式傳遞。
傳送波形如下圖所示:
NEC協議規定:
傳送協議資料“0” = 傳送載波560us + 不傳送載波560us
傳送協議資料“1” = 傳送載波560us+ 不傳送載波1680us
傳送引導碼 = 傳送載波9000us + 不傳送載波4500us
在紅外接收端,如果接收到紅外38K載波,則IR輸出為低電平,如果不是載波包括固定低電平和固定高電平則輸出高電平。在IR端接收的訊號如下所示:
三、軟體實現自學習
設計原理:
1、 根據接收波形記錄電平和電平持續時間,以便於傳送。
2、電平記錄採用定時器捕獲功能,從下降沿接收引導訊號開始,每觸發一次改變觸發方式,從而使每個電平變化都能捕獲到。
原始碼實現如下:
定時器捕獲初始化設定(CubeMax自動配置生成):
void MX_TIM4_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; TIM_IC_InitTypeDef sConfigIC = {0}; htim4.Instance = TIM4; htim4.Init.Prescaler = 71; htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.Period = 10000; htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim4) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } if (HAL_TIM_IC_Init(&htim4) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK) { Error_Handler(); } sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; sConfigIC.ICFilter = 0; if (HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_4) != HAL_OK) { Error_Handler(); } }
定時器捕獲中斷回撥處理:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_4) { if(TIM4->CCER & (TIM_CCER_CC4P)) //下降沿觸發 { TIM4->CCER &= ~(TIM_CCER_CC4P); //切換 gu8BitVal = 1; } else //上升沿觸發 { TIM4->CCER |= TIM_CCER_CC4P; //切換 gu8BitVal = 0; } if(gsInfrared.State == NONE_STATE) { gsInfrared.State = RECV_STATE; } else if(gsInfrared.State == RECV_STATE) { NowTimCnt = HAL_TIM_ReadCapturedValue(&htim4, TIM_CHANNEL_4); gsInfrared.KeepTime[gsInfrared.SampleCount] = Round(NowTimCnt); gsInfrared.BitValue[gsInfrared.SampleCount ++] = gu8BitVal; } TIM4->CNT = 0; } }
3、設定的定時器溢位時間為10ms,如果10毫秒內不再接收電平變化則預設接收結束,設定結束標誌。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim == &htim4) { if(gsInfrared.State == RECV_STATE) { gsInfrared.State = END_STATE; } } }
至此,實現了紅外遙控的學習功能,獲得的記錄資料為記錄長度和電平訊號陣列與電平訊號維持的時間陣列。
4、傳送實現
設定定時器輸出38KPWM訊號,在記錄電平為0是輸出記錄時間的38K載波訊號,如果為1則不輸出載波,實現如下:
PWM生成設定(CubeMax自動配置生成):
void MX_TIM5_Init(void) { TIM_MasterConfigTypeDef sMasterConfig = {0}; TIM_OC_InitTypeDef sConfigOC = {0}; htim5.Instance = TIM5; htim5.Init.Prescaler = 0; htim5.Init.CounterMode = TIM_COUNTERMODE_UP; htim5.Init.Period = 1896; htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_PWM_Init(&htim5) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK) { Error_Handler(); } sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; if (HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) { Error_Handler(); } HAL_TIM_MspPostInit(&htim5); }
傳送實現,注意點就是記錄為0時發載波,記錄為1時不發載波:
void InfraredSend(void) { uint16_t Count = 0; while(Count < gsInfrared.SampleCount && gsInfrared.State == END_STATE) { if(gsInfrared.BitValue[Count] == 0) { TIM5->CCR2 = 948; delay_us(gsInfrared.KeepTime[Count]); TIM5->CCR2 = 0; } else { TIM5->CCR2 = 0; delay_us(gsInfrared.KeepTime[Count]); TIM5->CCR2 = 0; } Count ++; } delay_us(20000); }
往PWM比較暫存器設定948即為設定38KPWM波,也可在初始化時固定948,在此函式內啟停定時器即可;
至此,自學習功能的全部思路已實現,通過對各個不同型別的紅外遙控進行功能測試,均成功。
PS:檢視很多資料發現很多紅外解碼未判斷低電平時間,個人感覺不是很好,應該是不僅高電平時間得符合,低電平時間也應該符合。
自己寫了一個小函式驗證了一下,這個函式只是驗證,未經仔細推敲,還可優化,僅供參考這一思想。
誤差設計:±200us(拍腦袋值)
void InFraredDataDeal(void) { uint32_t DataBuff = 0; uint16_t Count = 0; if(gsInfrared.State == END_STATE) { gsInfraredData.State = 0; do { switch(gsInfraredData.State) { case 0: //引導碼識別 { if(gsInfrared.KeepTime[0] >= 8800 && gsInfrared.KeepTime[0] <= 9200 && gsInfrared.BitValue[0] == 0) { if(gsInfrared.KeepTime[1] >= 4300 && gsInfrared.KeepTime[1] <= 4700 && gsInfrared.BitValue[1] == 1) { if(gsInfrared.KeepTime[2] >= 360 && gsInfrared.KeepTime[2] <= 760 && gsInfrared.BitValue[2] == 0) { Count = 3; gsInfraredData.State = 1; } } else if(gsInfrared.KeepTime[1] >= 2300 && gsInfrared.KeepTime[1] <= 2700 && gsInfrared.BitValue[1] == 1) { if(gsInfrared.KeepTime[2] >= 360 && gsInfrared.KeepTime[2] <= 760 && gsInfrared.BitValue[2] == 0) { gsInfraredData.ReDataCount ++; gsInfraredData.State = 3; } } else { gsInfraredData.State = 3; } } else { gsInfraredData.State = 3; } } break; case 1: //資料解析 { if(gsInfrared.KeepTime[Count + 1] >= 360 && gsInfrared.KeepTime[Count + 1] <= 760 && gsInfrared.BitValue[Count + 1] == 0) { if(gsInfrared.BitValue[Count] == 1) { if(gsInfrared.KeepTime[Count] >= 1480 && gsInfrared.KeepTime[Count] <= 1880) { DataBuff <<= 1; DataBuff |= 1; } else if(gsInfrared.KeepTime[Count] >= 360 && gsInfrared.KeepTime[Count] <= 760 && gsInfrared.BitValue[Count] == 1) { DataBuff <<= 1; DataBuff |= 0; } else { gsInfraredData.State = 3; } } } if(Count < gsInfrared.SampleCount) { Count += 2; } else { gsInfraredData.State = 2; } } break; case 2: //成功解析 { gsInfraredData.Data = DataBuff; gsInfraredData.State = 3; } break; default: { gsInfraredData.State = 3; //解析結束 } break; } } while(gsInfraredData.State != 3); gsInfrared.State = NONE_STATE; gsInfrared.SampleCount = 0; } }
解析的話一般高位在前,所以左移,經測試幀格式為:引導碼+使用者碼+使用者碼反碼+命令碼+命令碼反碼,能成功解析資料!解析的話根據具體協議,具體分析。
&n