戰(zhàn):EC11旋轉(zhuǎn)編碼器從硬件接線到狀態(tài)機(jī)消抖完整方案)
簡介一套面向嵌入式入門與進(jìn)階的STM32驅(qū)動(dòng)EC11編碼器程序源碼工程用于測量旋轉(zhuǎn)角度、速度與方向并通過串口將數(shù)據(jù)上傳至上位機(jī)。資源壓縮包共120個(gè)文件、約2.23MB以C語言源文件、頭文件、Keil工程配置文件和可執(zhí)行文件為主同時(shí)包含腳本輔助文件工程結(jié)構(gòu)按初始化、中斷服務(wù)、數(shù)據(jù)計(jì)算和串口通信等模塊劃分便于直接查看和復(fù)用。目前該資源已被4139人瀏覽學(xué)習(xí)特別適合想掌握編碼器信號(hào)處理、STM32中斷系統(tǒng)、定時(shí)器捕獲模式以及USART串口通信的開發(fā)者。通過這套源碼可以學(xué)習(xí)EC11編碼器A、B兩路相位差90度脈沖的采集與判斷方法以及從定時(shí)器捕獲到數(shù)據(jù)封裝的完整實(shí)現(xiàn)。配合調(diào)試器與示波器進(jìn)行信號(hào)驗(yàn)證還能理解底層外設(shè)驅(qū)動(dòng)與上位機(jī)數(shù)據(jù)交互的完整鏈路對(duì)嵌入式項(xiàng)目開發(fā)有直接的參考價(jià)值。 做儀器面板時(shí)我最常用的輸入設(shè)備不是觸摸屏而是旋鈕。調(diào)音臺(tái)的音量推子、示波器的菜單滾輪、3D打印機(jī)的微調(diào)旋鈕背后十有八九都是同一顆料——EC11編碼器。這顆幾塊錢的旋轉(zhuǎn)編碼器配上一塊STM32幾乎能解決所有“需要人手轉(zhuǎn)一下”的人機(jī)交互場景。這篇文章不聊空泛的概念直接圍繞EC11編碼器基于STM32程序源碼展開講清楚硬件怎么接、程序怎么寫、抖動(dòng)怎么消、高速旋轉(zhuǎn)為什么丟步以及最后怎么落成一個(gè)能用的交互組件。不管是做畢業(yè)設(shè)計(jì)、個(gè)人項(xiàng)目還是產(chǎn)品原型這套東西都能直接抄作業(yè)。1. 為什么旋鈕類產(chǎn)品都在用EC11看懂A/B相波形再談接線1.1 EC11引腳定義與內(nèi)部結(jié)構(gòu)EC11雖然是機(jī)械旋轉(zhuǎn)編碼器但它的核心不是“電位器式的電阻變化”而是兩個(gè)觸點(diǎn)開關(guān)按固定節(jié)奏通斷。常見的EC11有五個(gè)引腳其中三個(gè)是旋轉(zhuǎn)檢測用的A、B、C公共端另外兩個(gè)是編碼器自帶的按鍵開關(guān)引腳。也有不帶按鍵的型號(hào)面板上只需要旋鈕的話就選五腳版本需要按壓確認(rèn)就選帶開關(guān)的版本。按下旋鈕時(shí)按鍵引腳之間會(huì)導(dǎo)通這個(gè)邏輯和普通輕觸按鍵完全一樣所以電路和程序都可以復(fù)用按鍵處理的思路。旋轉(zhuǎn)的時(shí)候旋鈕內(nèi)部一個(gè)帶彈片的轉(zhuǎn)盤依次與固定觸點(diǎn)接觸A相和B相會(huì)交替導(dǎo)通和斷開產(chǎn)生兩路相位差剛好為90度的方波信號(hào)。1.2 波形特征與應(yīng)用價(jià)值相位差90度是理解整個(gè)編碼器的鑰匙。正轉(zhuǎn)時(shí)A相跳變沿領(lǐng)先B相90度反轉(zhuǎn)時(shí)B相領(lǐng)先A相90度MCU只需要檢測誰領(lǐng)先誰就能判斷旋轉(zhuǎn)方向。更妙的是這種“相位差”檢測方式比單純數(shù)脈沖更可靠即使旋鈕在某個(gè)位置輕微抖動(dòng)只要不跨越完整的相位狀態(tài)方向判定的結(jié)果也不會(huì)亂跳。EC11常見規(guī)格是一圈30個(gè)定位檔位也就是旋鈕轉(zhuǎn)一圈會(huì)發(fā)出30組A/B脈沖周期。實(shí)測時(shí)用示波器看A/B兩腳任意一腳都有明顯邊沿兩路信號(hào)存在穩(wěn)定的相位差這就說明編碼器本身工作正常問題大概率出在后級(jí)電路或軟件上。如果是用邏輯分析儀看更能清楚看到正轉(zhuǎn)時(shí)A相上升沿對(duì)應(yīng)B相是高還是低反轉(zhuǎn)時(shí)對(duì)應(yīng)關(guān)系正好反過來這正是后面軟件判向的基礎(chǔ)。2. 硬件抗干擾設(shè)計(jì)上拉電阻與濾波電容的參數(shù)選擇2.1 上拉方式選擇EC11的A/B輸出是開漏式的機(jī)械觸點(diǎn)必須配合上拉電阻才能輸出穩(wěn)定的高電平。選擇上拉電阻主要看兩個(gè)因素MCU供電電壓和信號(hào)頻率。3.3V供電的STM32一般推薦用10kΩ上拉到3.3V既保證觸點(diǎn)導(dǎo)通時(shí)電流不至于過大又能讓信號(hào)邊沿足夠陡峭。如果直接把EC11接到已經(jīng)配置了內(nèi)部上拉的GPIO上跑通Demo是可以的但抗干擾能力差得多。機(jī)械觸點(diǎn)導(dǎo)通瞬間的接觸電阻不穩(wěn)定內(nèi)部上拉太弱STM32內(nèi)部上拉通常在30kΩ~50kΩ信號(hào)會(huì)像一條拉不直的繩子邊沿變緩稍微有點(diǎn)外部干擾就誤觸發(fā)。我建議外部上拉電阻串聯(lián)在編碼器附近的PCB上而不是只依賴MCU內(nèi)部上拉。用內(nèi)部上拉做快速驗(yàn)證沒問題但產(chǎn)品級(jí)設(shè)計(jì)必須加外部上拉。2.2 濾波電容的取舍A/B線對(duì)地各加一個(gè)濾波電容是抑制機(jī)械抖動(dòng)和空間電磁干擾最直接的手段。電容值的選取有個(gè)容易走極端的坑太小起不到濾波作用太大把邊沿拉得太緩STM32的GPIO可能直接識(shí)別不出有效的電平跳變。我做過一組對(duì)比測試在F103開發(fā)板上分別接10nF、47nF、100nF的電容高速旋轉(zhuǎn)時(shí)觀察中斷計(jì)數(shù)10nF幾乎沒有明顯濾波效果波形上仍能看到毛刺47nF對(duì)一般轉(zhuǎn)速每秒10圈以內(nèi)表現(xiàn)最佳毛刺基本消失100nF在高速旋轉(zhuǎn)時(shí)上升沿變緩偶爾會(huì)丟脈沖所以一般建議取10nF到47nF之間如果編碼器離MCU很近、環(huán)境干擾不嚴(yán)重甚至可以不加電容靠軟件消抖來解決。轉(zhuǎn)速要求高的場景寧可用軟件消抖也不要加大電容。3. 三種讀取方案對(duì)比輪詢、外部中斷與定時(shí)器編碼器模式3.1 方案速覽讀取EC11的方式有三種工程中到底選哪種取決于主控的負(fù)載情況和旋轉(zhuǎn)速度要求。把三種方案放在一起看優(yōu)劣非常明顯。方案CPU開銷實(shí)時(shí)性代碼復(fù)雜度適用場景主循環(huán)輪詢高差低Demo驗(yàn)證、低速旋鈕外部中斷低好中大多數(shù)產(chǎn)品場景定時(shí)器編碼器模式極低硬件處理極好中配置稍繁瑣高速旋轉(zhuǎn)、多組編碼器輪詢最大的問題不是“讀不出來”而是主循環(huán)里一旦有延時(shí)、刷屏、通信這類阻塞操作編碼器脈沖就會(huì)堆積丟步概率直線上升。所以只要項(xiàng)目稍微有點(diǎn)復(fù)雜度我都不推薦主循環(huán)輪詢方案。3.2 定時(shí)器編碼器模式的配置STM32的通用定時(shí)器和高級(jí)定時(shí)器幾乎都帶編碼器接口官方手冊(cè)里叫Encoder Mode本質(zhì)上是一個(gè)硬件正交解碼器。它直接同時(shí)監(jiān)測A/B兩相把脈沖數(shù)和方向解算成計(jì)數(shù)器的增減程序只需要讀計(jì)數(shù)器值就能知道轉(zhuǎn)了多少格。CubeMX里的配置非常簡單將定時(shí)器的CH1和CH2分別映射到A相和B相引腳選擇Encoder Mode根據(jù)需要的計(jì)數(shù)方向配置TI1和TI2的極性設(shè)置計(jì)數(shù)器上限比如開成16位自動(dòng)重裝或者改成32位計(jì)數(shù)模式關(guān)鍵一步Slave Mode選Encoder ModeInput Capture預(yù)分頻設(shè)為不分頻使能定時(shí)器之后硬件就會(huì)自動(dòng)根據(jù)A/B相順序加減計(jì)數(shù)器。正轉(zhuǎn)一圈計(jì)數(shù)增加N反轉(zhuǎn)一圈減少N這個(gè)N就是單圈脈沖數(shù)的四倍頻結(jié)果。EC11本身30個(gè)脈沖周期四倍頻后一圈120個(gè)計(jì)數(shù)步進(jìn)手感細(xì)膩很多。這個(gè)方案我強(qiáng)烈推薦它不用寫中斷處理函數(shù)不用手動(dòng)管理消抖狀態(tài)CPU占用接近零轉(zhuǎn)速上限遠(yuǎn)高于GPIO中斷方式。唯一的門檻是CubeMX配置時(shí)要稍微理解正交解碼的含義但配置過一次之后你會(huì)覺得這才是STM32對(duì)EC11最優(yōu)雅的解法。4. 狀態(tài)機(jī)消抖與方向判定核心源碼逐段解析4.1 狀態(tài)機(jī)方向判定思路如果不用定時(shí)器編碼器模式而是用GPIO外部中斷那狀態(tài)機(jī)消抖和方向判定就必須自己實(shí)現(xiàn)。很多人第一反應(yīng)是檢測A相上升沿此時(shí)讀B相電平高就是正轉(zhuǎn)低就是反轉(zhuǎn)。這個(gè)方法能工作但高速旋轉(zhuǎn)時(shí)抖動(dòng)會(huì)讓邊沿重復(fù)觸發(fā)方向判定容易出錯(cuò)。更可靠的做法是把A/B兩相合成一個(gè)2位狀態(tài)值每一位對(duì)應(yīng)一相信號(hào)然后維護(hù)一個(gè)“上一次狀態(tài)當(dāng)前狀態(tài)”的轉(zhuǎn)移表。正轉(zhuǎn)時(shí)狀態(tài)按00→01→11→10→00循環(huán)反轉(zhuǎn)時(shí)按00→10→11→01→00循環(huán)。任何不屬于這兩個(gè)循環(huán)的跳變都視為抖動(dòng)直接忽略。這種狀態(tài)機(jī)天然抗抖因?yàn)樗笮盘?hào)必須按合法順序切換才算有效步進(jìn)。4.2 具體代碼實(shí)現(xiàn)以HAL庫為例A相接PA0B相接PA1兩個(gè)引腳都配置為外部中斷。中斷回調(diào)里調(diào)用狀態(tài)機(jī)函數(shù)#define EC11_A_PIN GPIO_PIN_0 #define EC11_B_PIN GPIO_PIN_1 #define EC11_PORT GPIOA static uint8_t ec11_last_state 0; volatile int16_t ec11_count 0; void EC11_Process(void) { uint8_t a HAL_GPIO_ReadPin(EC11_PORT, EC11_A_PIN); uint8_t b HAL_GPIO_ReadPin(EC11_PORT, EC11_B_PIN); uint8_t cur_state (a 1) | b; uint8_t trans (ec11_last_state 2) | cur_state; ec11_last_state cur_state; switch (trans) { case 0x01: // 00 - 01 case 0x07: // 01 - 11 case 0x0E: // 11 - 10 case 0x08: // 10 - 00 ec11_count; break; case 0x02: // 00 - 10 case 0x04: // 01 - 00 case 0x0B: // 10 - 11 case 0x0D: // 11 - 01 ec11_count--; break; default: break; // 抖動(dòng)或非法跳變忽略 } } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if ((GPIO_Pin EC11_A_PIN) || (GPIO_Pin EC11_B_PIN)) { EC11_Process(); } }這段代碼的關(guān)鍵在于避免在中斷里做任何耗時(shí)的操作。EC11_Process里只有讀引腳、拼狀態(tài)、查表增減計(jì)數(shù)全程沒有延時(shí)、沒有printf、沒有除法中斷里執(zhí)行時(shí)間在微秒級(jí)別完全可控。4.3 為什么不用簡單的邊沿電平判斷只檢測A相邊沿再讀B相在低速時(shí)表現(xiàn)很好但高速旋轉(zhuǎn)時(shí)問題很隱蔽邊沿處B相可能正處于抖動(dòng)毛刺區(qū)讀到的高或低是不穩(wěn)定的方向就會(huì)間歇性誤判。狀態(tài)機(jī)轉(zhuǎn)移表要求完整的“四步走”序列任何一步?jīng)]走完都不計(jì)數(shù)等于把四分之一周期內(nèi)的所有抖動(dòng)都吸收掉了抗干擾能力完全不在一個(gè)級(jí)別。實(shí)測一種工況手捏旋鈕快速來回搓動(dòng)邊沿電平方案會(huì)出現(xiàn)次數(shù)計(jì)數(shù)原地反彈而狀態(tài)機(jī)方案計(jì)數(shù)基本保持在很小的范圍內(nèi)波動(dòng)。這就是為什么產(chǎn)品里幾乎都用狀態(tài)機(jī)或者硬件編碼器模式而不是“檢測上升沿讀另一個(gè)腳”這種入門方案。5. 我踩過的高速丟步與誤觸發(fā)問題完整排查鏈路5.1 問題現(xiàn)象與復(fù)現(xiàn)條件有一次用GPIO中斷做音量旋鈕低速旋轉(zhuǎn)完全正常但快速擰的時(shí)候計(jì)數(shù)不是線性增加而是偶爾少記一格甚至方向反一下。這個(gè)現(xiàn)象用示波器看波形A/B相位關(guān)系其實(shí)是正確的問題不出在編碼器本體。復(fù)現(xiàn)條件很有意思手?jǐn)Q速度非常快、中斷函數(shù)里恰好在處理別的標(biāo)志位、主循環(huán)再調(diào)用一次HAL_Delay導(dǎo)致中斷響應(yīng)被拉長。三個(gè)條件湊齊丟步就會(huì)穩(wěn)定復(fù)現(xiàn)。5.2 排查過程第一步先看中斷是否丟失。在中斷回調(diào)里加一個(gè)計(jì)數(shù)器和主循環(huán)里的另一路獨(dú)立計(jì)數(shù)對(duì)比發(fā)現(xiàn)中斷發(fā)生次數(shù)明顯少于邏輯分析儀抓到的脈沖數(shù)說明中斷本身就有丟失。第二步查中斷優(yōu)先級(jí)。默認(rèn)配置下GPIO中斷優(yōu)先級(jí)可能和其他外設(shè)沖突F103這類Cortex-M3內(nèi)核如果中斷里調(diào)用了HAL_Delay這種依賴SysTick的阻塞函數(shù)SysTick優(yōu)先級(jí)低于外部中斷時(shí)就會(huì)死等等效于關(guān)閉了中斷響應(yīng)。這就是很多新手中斷卡死的原因。第三步看GPIO濾波相關(guān)配置。CubeMX里GPIO可以配置施密特觸發(fā)器輸入內(nèi)部還會(huì)有模擬濾波這些默認(rèn)配置一般不用動(dòng)但如果之前為了省電把引腳配成了模擬模式那就完全讀不到數(shù)字電平了。5.3 最終修復(fù)修復(fù)措施分三個(gè)層次把EC11兩個(gè)引腳的中斷優(yōu)先級(jí)設(shè)為盡可能高且其中不要調(diào)用任何阻塞函數(shù)或HAL_Delay中斷回調(diào)里只做狀態(tài)機(jī)更新和計(jì)數(shù)增減所有后續(xù)處理放到主循環(huán)查詢標(biāo)志位改用定時(shí)器編碼器模式從根源上消除中斷丟失問題修復(fù)后同一測試場景連續(xù)快速正反旋轉(zhuǎn)計(jì)數(shù)完全線性不再出現(xiàn)丟步和反轉(zhuǎn)。這里要特別提醒如果你的項(xiàng)目里已經(jīng)用了FreeRTOS這類RTOS中斷和任務(wù)之間的數(shù)據(jù)交互要注意加臨界區(qū)保護(hù)或者關(guān)中斷訪問共享變量否則高優(yōu)先級(jí)任務(wù)搶占時(shí)同樣會(huì)出現(xiàn)偶發(fā)計(jì)數(shù)錯(cuò)誤。6. 進(jìn)階把編碼器變成產(chǎn)品級(jí)交互組件6.1 音量調(diào)節(jié)與加速算法拿到干凈的計(jì)數(shù)后不能直接把計(jì)數(shù)值映射成音量還得加“加速”邏輯。慢速旋轉(zhuǎn)時(shí)一格一格調(diào)適合精細(xì)調(diào)節(jié)快速旋轉(zhuǎn)時(shí)一次跳多格適合從0拉到滿。這是很多產(chǎn)品音量旋鈕的手感來源。實(shí)現(xiàn)思路很簡單static uint32_t last_tick 0; static uint32_t last_count 0; uint16_t get_volume_step(void) { uint32_t now HAL_GetTick(); uint32_t dt now - last_tick; int16_t delta ec11_count - last_count; last_tick now; last_count ec11_count; if (dt 200) return delta; // 慢轉(zhuǎn)一格算一格 if (dt 50) return delta * 4; // 中等速度加速4倍 return delta * 10; // 快速加速10倍 }實(shí)際上就是把時(shí)間間隔和計(jì)數(shù)增量做一個(gè)線性映射時(shí)間越短振幅越大。這個(gè)算法實(shí)測手感很自然比固定步進(jìn)的方案好用太多。要注意返回值用有符號(hào)數(shù)反轉(zhuǎn)時(shí)才能正確減小音量。6.2 菜單翻頁與按鍵復(fù)用EC11自帶按鍵和旋轉(zhuǎn)兩路輸入組合起來可以覆蓋很多交互場景。一個(gè)經(jīng)典應(yīng)用是“旋轉(zhuǎn)選擇菜單按下確認(rèn)長按返回”。按鍵邏輯單獨(dú)復(fù)用一組狀態(tài)機(jī)短按觸發(fā)確認(rèn)事件長按超過1秒觸發(fā)返回事件松開時(shí)清零計(jì)時(shí)。這里有個(gè)容易踩坑的地方長按和短按的判定不能放在同一個(gè)中斷回調(diào)里延時(shí)判斷否則旋鈕旋轉(zhuǎn)的響應(yīng)會(huì)被卡住。正確的做法是中斷里只記錄按鍵按下和松開的時(shí)間戳主循環(huán)里用非阻塞方式判斷時(shí)間差。6.3 PID參數(shù)調(diào)節(jié)與多參數(shù)設(shè)置做電機(jī)控制或者溫控項(xiàng)目時(shí)用EC11調(diào)PID參數(shù)是非常順手的方案。比如三個(gè)參數(shù)P、I、D分別由三頁菜單承載旋轉(zhuǎn)編碼器選中一個(gè)參數(shù)按下切換到編輯模式再旋轉(zhuǎn)調(diào)整數(shù)值。得益于定時(shí)器編碼器模式的精確計(jì)數(shù)參數(shù)調(diào)節(jié)能做到每次旋轉(zhuǎn)僅增減一個(gè)最小步進(jìn)配合加速算法還能快速跨越量程。我實(shí)際測試過用EC11配合串口打印PID參數(shù)調(diào)到目標(biāo)值后再寫入Flash保存整個(gè)過程不需要接電腦改代碼重新燒錄調(diào)試效率比改宏定義再編譯高出太多。如果你的項(xiàng)目里有多個(gè)參數(shù)要現(xiàn)場整定強(qiáng)烈建議把EC11做成通用參數(shù)調(diào)節(jié)組件。關(guān)于EC11還有一個(gè)容易被忽略的點(diǎn)它的機(jī)械壽命標(biāo)稱通常在三萬次旋轉(zhuǎn)循環(huán)左右聽起來很多但產(chǎn)品設(shè)計(jì)時(shí)如果旋鈕被用戶無意識(shí)反復(fù)搓動(dòng)壽命消耗比想象中快。所以結(jié)構(gòu)設(shè)計(jì)上可以考慮加阻尼或防誤觸結(jié)構(gòu)程序上也盡量用邊沿計(jì)數(shù)而不是輪詢減少無意義的中斷喚醒。寫到這里EC11從硬件到軟件到產(chǎn)品集成的完整鏈路基本都過了一遍。我個(gè)人實(shí)際項(xiàng)目中用得最多的組合是定時(shí)器編碼器模式加狀態(tài)機(jī)按鍵整個(gè)交互模塊只有兩個(gè)引腳加一個(gè)按鍵引腳代碼量精簡又不占用CPU后續(xù)無論接LCD菜單、藍(lán)牙調(diào)參還是USB聲卡音量旋鈕都能直接復(fù)用這套底層邏輯。如果你的項(xiàng)目還在用輪詢和邊沿檢測不妨花一晚上換成這套方案手感提升是立竿見影的。本文還有配套的精品資源點(diǎn)擊獲取