NEC格式的紅外編碼
調製
NEC格式採用脈衝間隔調製技術,脈衝週期不變,改變邏輯‘1’和邏輯‘0’的脈衝佔空比。
時序信息:
● 載波:38 kHz 1/3或1/4佔空比
● 有效脈衝:560 uS
● 邏輯週期:
○ 邏輯 ‘1’:2.25ms
○ 邏輯 ‘0’:1.12ms
協議
NEC協議每次傳輸包含8-bit命令碼和8-bit地址碼。在發送命令碼和地址碼之前,會先發送前導碼(同步碼),週期13.5ms,有效脈衝週期9ms。爲了提高可靠性,每個命令碼和地址碼後面都會跟它自己的反碼。低位在前高位在後。
重複序列
命令碼和地址碼協議幀只發送一次,之後開始發送重複序列,重複週期108ms。重複序列包含一個前導碼和一個停止位,前導碼週期11.25ms,其中有效脈衝週期9ms,重複幀前導碼比數據地址幀的前導碼要短。
填坑
基本思路
BLE協議棧跑在CC2541上,有很多廣播、通知和連接間隔需要時不時去處理一下,所以紅外接收必然不能佔用太多時間,如果採用阻塞接收的話,最快也得接收個40+ms的樣子,很容易就把GAP給搞失聯了。
因此,爲了避免踩坑,採用中斷捕獲的方式來採集紅外信號,osal是事件驅動的一個任務調度系統,爲了使軟件更好的分層,同時防止中斷佔用太多時間,在採集到完整紅外數據後給硬件事件處理任務發送一個紅外接收完成事件,HAL_TaskID的事件處理函數中對接收到的紅外數據進行解碼檢驗,之後是否需要封裝成紅外消息發送到用戶的任務進行更進一步處理,就看個人需求了。
需求分析
紅外測序必然需要用到一個定時器,CC2541用戶能用的就三個定時器:Timer1(16bit)、Timer3和Timer4(8bit)。還有一個Timer2被協議棧吃了,Ti也沒打算給我們用,甚至用戶手冊裏都沒寫Timer2的詳細說明。Timer1功能強大,總不能爲了驅動一個紅外把它犧牲了,T3和T4一樣那就隨便選一個好了,我選擇T4。
在預先軟件設計中,打算利用T4的雙邊沿捕獲功能得到高電平脈寬,測得前導碼(4.5ms),邏輯‘1’(1.69ms),邏輯‘0’(560us)和重複序列,由於這幾個特性信號的週期跨度很大,隨便比較一下就能快速獲得32bit命令碼和地址碼。
理想的世界裏處理任何事情總是一帆風順的,但現實是,T4只有8bit計數器,時鐘頻率32M,最大分頻128,就算溢出也只有1024us,不說前導碼4.5ms,連邏輯‘1’都捕獲不到,此刻我內心一片淒涼,難道如此輕易就要出動我的最強T1了嗎,不禁吶喊arm核牛逼,51辣雞!
這點小困難就要出動最強T1?我的內心是當然是醜拒的。因此,我必須要開拓思路,給醜陋的8bit T4再安上一個16bit的計數器,從此它短小的人生就此被改寫~,它站起來了,挺直了腰桿。
利用T4的雙沿中斷+溢出中斷+額外16bit變量,就能準確地測出整個紅外信號中所有高電平脈衝的時間,想到這裏我不禁如釋重負,緩緩舒了口氣,就在我上半身不自覺徐徐靠向了座椅的懷抱之際,腦海中突然炸開一到閃電,雙沿中斷+溢出中斷,2+1那不就需要三個中斷才能實現我原先只要兩個中斷就能完成的設想,此刻我內心一片淒涼,難道如此輕易就要出動我的最強T1了嗎,不禁吶喊~~~~不對,這次51不背這個鍋。
老師從小教導我們,只要思想不滑坡,方法總比困難多。我又一次不得不另闢蹊徑,回頭分析了一波紅外信號的波形。由於一體化接收頭解調後的信號是反相的,最終輸出的波形跟上文的調製信號是正好相反的。經過我的仔細分析,數據幀的邊沿還是挺多的,利用雙沿來採集高電平脈衝的時間,不僅效率不高,軟件也更復雜。我不禁陷入了深深的思考,那能不能只用一個邊沿來測量呢?突然,我發現紅外信號起始部分是一個下降沿,而前導碼,邏輯‘1’和‘0’,停止碼都是有固定週期的,比如前導碼就是9ms低電平+4.5ms高電平,這一發現使我心情一下子豁然開朗,如此一來事情就變的簡單多了,我根本不需要去測量高電平的時間,直接測量這幾個特徵信號的完整週期就能準確完成信號解碼,而T4只需要配置下降沿中斷+溢出中斷,想到這裏我的內心不禁笑出了豬叫聲,配合我雲淡風輕的面部表情,不禁感嘆:技術人的生活,就是這樣樸實無華且枯燥。
子曰:有道無術則怠,有術無道則惘,道術相依方能始終。經過前文的激烈分析,接下來就要開始求證階段了,看看需求是否能按計劃中那樣實現。
正文
定時器初始化
正文就不囉嗦了直接貼代碼。首先是初始化函數,簡單先註冊一下回調函數,下文會提到。
void HalTimer4Init (void)
{
//Initialize TIM4
HalTimerConfig(HAL_TIMER_4,
HAL_TIMER_MODE_NORMAL,
HAL_T4_CH0,
HAL_TIMER_CH_MODE_INPUT_CAPTURE,
TRUE,
HalIrDecode);
}
配置T4,使用T4的CH0作爲紅外信號捕獲引腳,由於硬件原因,我把T4的兩個通道替換到了Alt2。
P2_0引腳作爲外設引腳
T4CCTL0配置爲Capture輸入捕獲+下降沿捕獲+通道中斷使能
T4CTL配置爲128分頻+計數器自由向上計數+溢出中斷使能+計數器清零
注意,T4CNT計數器只能讀不能寫,所以不能寫零清零,只能通過寫T4CTL_CLR 這個位來清零,這是個大坑,我纔不會告訴你我掉進去過
uint8 HalTimerConfig ( uint8 timerId,
uint8 opMode,
uint8 channel,
uint8 channelMode,
bool intEnable,
halTimerCBack_t cback )
{
uint8 error = HAL_TIMER_OK;
(void)timerId;
(void)opMode;
(void)channel;
(void)intEnable;
//P2DIR |= 0x01;
/* Alternative 2 location */
PERCFG |= PERCFG_T4CFG;
/* Selects P2_0 as peripheral I/O */
P2SEL |= P2SEL_SELP2_0;
//PWM setup
// T4CNT = 0;
//
// T4CCTL0 = T4CCTLn_CMP_SET_CMP_UP_CLR_0 | T4CCTLn_MODE;
//
// T4CC0 = 128;
//
// T4CTL = T4CTL_DIV_128 | T4CTL_START | T4CTL_OVFIM |
// T4CTL_CLR | T4CTL_MODE_FREERUN;
/***************************************************************************
* Setup interrupt
*
* Enables global interrupts (IEN0.EA = 1) and interrupts from Timer 4
* (IEN1.T3IE = 1).
*/
T4IE = 1;
/* Timer 4 channel 1 compare control configuration. interrupt enable*/
T4CCTL0 |= T4CCTLn_CAP_FALL_EDGE | T4CCTLn_IM;
/* Timer 4 control. Sets the prescaler divider value to 128, starts the Timer,
* disables overflow interrupts, clears the Timer and sets the mode to
* up-down.
*/
T4CTL = T4CTL_DIV_128 | T4CTL_CLR | T4CTL_MODE_FREERUN |T4CTL_OVFIM;
pHalTimerProcessFunction = cback;
return error;
}
中斷處理
不做過多解釋,中斷來了就送進回調裏處理
/**************************************************************************************************
* @fn halTIM4Isr
*
* @brief TIM4Isr
*
* @param
*
* @return
**************************************************************************************************/
HAL_ISR_FUNCTION( halTIM4Isr, T4_VECTOR )
{
uint8 state = 0;
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState);
if(T4OVFIF)
{
state |= TIM4_OVIF;
T4OVFIF = 0;
}
if(T4CH0IF)
{
state |= TIM4_CH0_FALL;
T4CH0IF = 0;
}
pHalTimerProcessFunction(state);
T4IF = 0;
HAL_EXIT_CRITICAL_SECTION(intState);
return;
}
回調
處理過程已經很精簡了,配合上文的圖分析一下應該問題不大。
放幾張圖吧,實際測試得到的完整的數據幀中每一bit的時間,recbuf[0]=3361,13.444ms基本就等於數據幀前導碼的週期11.45ms;recbuf[1]=281,1.12ms基本等於邏輯零的週期。正體看下來數據的一致性很高,並沒有我當初想想中會被RF中斷打斷導致不準確的問題產生,基本上問題不大。
接着分析,到數組第32個元素爲止,有效數據已經算是全部接收完成了,圖片選用了一個比較容易觀察的紅外按鍵數據0x00FF00FF。recbuf[33]=10143表示數據幀的108ms中,除去有效載荷部分之外的剩餘部分,這部分是變化的,取決與命令和數據碼’0‘’1‘比例,不過基本上也就是在10143上下左右浮動,不會變化太大,畢竟載荷全0x00和全0xFF是不存在(我也不知道存不存在)。之後就是重複序列的引導碼和停止碼,只要一直按着遙控不放,這兩部分就會重複出現。
至於是邏輯’0‘、邏輯’1‘、引導碼,還是重複序列,根據上面兩張週期圖推算以下就行了,各自之間的時間跨度還是很大的,很容易就能區分開來。
NECData_t irdata;
uint8 RmtSta = 0;
uint16 CapVal = 0;
uint32 RmtRec = 0;
uint8 RmtCnt = 0;
uint16 recbuf[40];
uint8 i =0;
/*********************************************************************
* @fn HalIrDecode
*
* @brief Callback service for TIM4
*
* @param capMode - Timer 4 Capture mode
*
* @return void
*********************************************************************/
void HalIrDecode(uint8 capMode)
{
if(capMode == TIM4_OVIF)
{
CapVal += 256;
if(RmtSta && STA_PREAMBLE)
{
if(CapVal > 25000) //More than STOP period of Repeat Sequence(11.25+96.75) 100 > 96.75
{
//The remote key release
//then stop tim4
T4CTL &= ~T4CTL_START;
//clear STA_SUCCESS
RmtSta = 0;
return;
}
}
}
if(capMode == TIM4_CH0_FALL)
{
CapVal += T4CC0;
if(RmtSta & STA_PREAMBLE)
{
if(CapVal > 250 && CapVal < 310) //the standard logic '0' is 1.12
{
RmtRec <<= 1;
}
else if(CapVal > 530 && CapVal < 590) //the standrad logic '1' is 2.25
{
RmtRec <<= 1;
RmtRec |= 1;
}
else if(CapVal > 7500 && CapVal < 12500) //rest of the preamble frame between 22.5 - 58.66
{
RmtSta |= STA_SUCCESS;
osal_set_event(Hal_TaskID,HAL_IR_EVENT);
}
else if(CapVal > 2500 && CapVal < 3000)
{
RmtCnt ++;
}
}
else if(CapVal > 3250 && CapVal < 3500) //the standard preamble cycle is 9+4.5
{
RmtSta |= STA_PREAMBLE;
RmtCnt = 0;
}
T4CTL |= T4CTL_CLR | T4CTL_START;
CapVal = 0;
}
}
紅外事件處理
上文給硬件任務發個紅外事件,硬件事件處理任務將全權接管接下來的所有操作,完成數據有效性的驗證,至於最終數據發送去哪個任務就看之後的用戶應用程序了。
uint16 Hal_ProcessEvent( uint8 task_id, uint16 events )
{
...........
if( events & HAL_IR_EVENT )
{
#if (defined HAL_IR) && (HAL_IR == TRUE)
HalIrProcess();
#endif
return events ^ HAL_IR_EVENT;
}
..........
}
/*********************************************************************
* @fn HalIrGetValue
*
* @brief ir code process
*
* @param none
*
* @return none
*********************************************************************/
void HalIrProcess(void)
{
if(RmtSta & STA_SUCCESS)
{
RmtSta &= ~STA_SUCCESS;
osal_memcpy((uint8 *)&irdata, (uint8 *)&RmtRec, 4);
if(irdata.addr ^ irdata.addr_inv == 0xFF)
{
if(irdata.cmd ^ irdata.cmd_inv == 0xFF)
{
}
}
}
}
總結
坑基本上算填完了,雖然寫了這麼多,但算算有效代碼也就100+左右,正所謂吹逼兩小時,代碼五分鐘,技術人的生活,就是這樣樸實無華且枯燥。
11月15日 于田裏