robot-notes — 機器人知識筆記 GitHub ↗
本章與本頁目次

核心

robot-notes /核心/硬體/編碼器

編碼器(Encoder)

編碼器是機器人「知道輪子轉了多少」的眼睛,odometry(里程定位)與閉迴路速度控制都靠它。本篇講霍爾編碼器與增量式 A/B 相的原理,以及 STM32F4 如何在硬體層自動數脈衝、軟體怎麼讀。

章節編號沿用原始《送餐機器人基礎原理補充》,方便與舊文件對照(故本檔編號不連續,如 §1→§5→§10,非缺漏)。 延伸閱讀:馬達與 FOC下位機運動控制定位


11. 霍爾編碼器:原理與 STM32F4 連接

11.1 原理:三顆霍爾開關 + 轉子磁鐵

先把兩個之後一路要用的詞釘住,不然後面的數字會看不懂。

極對數:轉子上那圈永久磁鐵是 N/S 交替排列的,成對算。輪轂馬達常見 15 對。

電氣角:磁場的圖樣每經過一對磁鐵就重複一次。所以輪子在物理上轉一圈(機械角 360°),馬達在電上經歷的是 15 圈——這個放大過的角度叫電氣角,電氣角 = 機械角 × 極對數

為什麼要分這兩種角度:馬達的控制邏輯(何時該換相、Park 變換要轉多少)看的全是磁場的相對位置,也就是電氣角;而 odometry 要的是輪子實際轉了多少,是機械角。兩者差 15 倍,搞混就是 15 倍的誤差。 下面除非特別說明,講的都是電氣角。

BLDC 定子上嵌三顆霍爾開關(HA、HB、HC),彼此相隔 120° 電氣角。轉子永磁體掃過時,每顆輸出 0 或 1(N 極靠近 = 1)。三個位元組合出 8 種狀態,其中 000111 物理上不會出現,有效狀態 6 個,轉子每轉過 60° 電氣角切換一次:

正轉狀態序列(典型):
  HA HB HC   狀態值
   1  0  1  → 5
   1  0  0  → 4         每個箭頭 = 60° 電氣角
   1  1  0  → 6         反轉就是同一序列倒著走
   0  1  0  → 2         → 看「下一個狀態」就知道方向
   0  1  1  → 3
   0  0  1  → 1
   (回到 5,循環)

解析度換算(關鍵):6 個狀態是「每電氣圈」;機械一圈 = 電氣圈數 × 極對數。輪轂馬達極對數多(常見 15 對),所以:

每機械圈狀態數 = 6 × 15 = 90 → 角解析度 = 360°/90 = 4°
輪徑 15cm → 一格 ≈ 5.2mm 地面距離

對比 §1.5:這就是「霍爾 = 低解析度編碼器」的由來——做速度閉迴路夠用(中高速時),做精細 odometry 偏粗,所以講究的方案會在輪轂馬達內加裝磁編碼器

它的雙重身分:同一組訊號,既給驅動器做換相(知道該給哪兩相通電),又可當編碼器(數狀態變化次數 = 角度;量狀態間隔時間 = 速度)。

11.2 接線:先弄清楚訊號從哪裡出來

輪轂馬達引出線通常是「3 粗 + 5 細」:

3 條粗線:U / V / W 三相動力線 → 接 FOC 驅動器功率端
5 條細線:VCC(常見 5V)、GND、HA、HB、HC → 訊號線

架構決定接到哪:

方案 霍爾線接到 STM32 怎麼拿到速度
買現成 FOC 驅動器(本專案建議) 接驅動器,它自己用來換相+測速 走 CAN 讀驅動器回報的轉速/位置——STM32 根本不直接接霍爾線
自研驅動 / 額外獨立測速 接 STM32 下述兩種讀法

以下 §11.3 適用於第二種情境,或單純理解原理。

11.3 STM32F4 硬體連接

輪轂馬達霍爾線 HA/HB/HC 經 10kΩ 上拉電阻接到 STM32F407 PB6/PB7/PB8(TIM4)

接線要點:

  1. 上拉電阻必要:霍爾開關輸出幾乎都是 open-collector / open-drain(只能拉低,不能推高),不上拉就永遠讀到不定值。上拉到 3.3V(即使霍爾供電 5V,open-drain 輸出上拉到 3.3V 就安全;若該霍爾板是推挽 5V 輸出,則要確認所用腳位為 5V-tolerant)。
  2. 訊號線跟三相動力線分開走線/隔開,動力線的 PWM 開關噪聲很容易耦合進來;必要時霍爾線用遮蔽線。
  3. 三個訊號接到同一顆 Timer 的 CH1/CH2/CH3(如 TIM4 的 PB6/PB7/PB8),才能用下面的硬體霍爾介面模式。

11.4 軟體讀法(兩種)

方法 A:Timer 霍爾感測器介面模式(硬體支援,建議)

STM32 Timer 有專門的 Hall sensor interface:把 CH1/CH2/CH3 三路訊號硬體 XOR 成一路接到內部 TI1,任何一顆霍爾翻轉 → 產生一次 capture 事件 + 計數器自動歸零:

三顆霍爾訊號硬體 XOR 進 TIM4 TI1,用 input capture 量出翻轉間隔時間

方法 B:三支 GPIO + EXTI 中斷(簡單直觀)

三支腳都設「雙緣觸發 EXTI」,中斷裡讀出 3-bit 狀態,查表:

// 正轉序列 5→4→6→2→3→1,做成查找表
static const int8_t hall_dir[8][8] = { /* [prev][curr] = +1 / -1 / 0(非法跳變) */ };

void EXTI_IRQHandler(void) {
    uint8_t curr = (HA << 2) | (HB << 1) | HC;
    position += hall_dir[prev][curr];   // 累積 count → odometry
    prev = curr;                        // 非法跳變(跳 2 格)→ 記異常計數
}

取捨:方法 B 好懂、好移植,但高轉速時中斷頻率 = 6 × 極對數 × 轉速(15 對極、300 RPM → 每輪每秒 2700 次中斷),兩顆輪子加起來 CPU 負擔可觀;方法 A 把計時歸零全交給硬體,CPU 只在需要時讀結果。正式韌體用方法 A,快速驗證接線用方法 B。

11.5 與本專案架構的對應

再強調一次邊界:採用「現成 FOC 驅動器 + CAN」的建議架構下,霍爾線接驅動器,STM32 不碰這五條線——轉速與累積位置由驅動器經 CAN 回報,STM32 拿來做運動學正解與 odometry 即可。本節的價值在於:(1) 看懂驅動器規格書裡的霍爾參數(極對數、霍爾角度 120°/60°);(2) 現場除錯時能用三用電表/邏輯分析儀直接驗證霍爾訊號;(3) 若日後自研驅動或加裝獨立編碼器,知道從哪裡下手。


19. Encoder 圖解:增量式 A/B 相如何運作

19.1 光學增量式編碼器的構造

光學增量式編碼器側視光路與正視碼盤構造,含 Z 相歸零縫

磁編碼器原理相同,只是把「光+碼盤」換成「磁鐵+磁感應晶片」,抗油污粉塵(輪轂馬達內建的就是這種);霍爾(§11)則可視為極粗的磁編碼器。

19.2 為什麼要兩個訊號(A/B 相)?——方向

A、B 兩個接收器在空間上錯開 1/4 個縫距,所以輸出方波相位差 90°:

A/B 正交方波正轉與反轉對照,在 A 上升緣讀 B 值判斷方向

只有一相只能數「轉了多少」,不知道往哪轉;90° 相位差就是方向資訊

19.3 ×4 解碼:解析度免費變四倍

一個縫距內,A/B 共有 4 個邊緣(A↑、B↑、A↓、B↓),每個邊緣都計一次數:

A/B 四態循環狀態環:正轉 00→10→11→01,×4 解碼達 4096 counts/圈

規格書寫「1024 PPR (pulses per revolution)」,經 ×4 解碼後實際可用 4096 CPR (counts per revolution)——§10.3 粗算用的 4096 就是這麼來的。

19.4 增量式 vs 絕對式(知道差別即可)

  增量式(A/B/Z) 絕對式(§10 伺服那種 17-bit)
開機時知道角度嗎 ✗ 只知道「之後轉了多少」 ✓ 每個角度有唯一編碼
訊號 兩路方波 數位介面(SSI/BiSS)
價格
底盤適用 ✓ 速度環+odometry 都只需要「增量」 不必要

底盤只關心轉速與位移增量,開機絕對角度無所謂——所以增量式就是正解。


20. Encoder 回授與下位機:STM32 程式實例

20.1 回授迴路全貌(資料在下位機裡怎麼流)

速度閉迴路方塊圖:上位機指令經 PID 到馬達,編碼器內回授 PID、外回報 odometry

encoder 在這個迴路裡身兼兩職:對內當 PID 的回授(快迴路,留在下位機),對外供 odometry(慢資料,上報上位機)。這就是「encoder 回授跟 STM32 的關聯」的全貌。

20.2 第一步:讓硬體自己數,CPU 不介入

STM32 Timer 的 encoder mode 把 §19.3 的 ×4 解碼做在硬體裡:A/B 接同一顆 Timer 的 CH1/CH2,計數器 CNT 自動加減(正轉加、反轉減),CPU 完全不用處理每個脈衝:

// 左輪 encoder:TIM3,A 相 = PA6 (CH1),B 相 = PA7 (CH2)
void Encoder_Init(void)
{
    TIM_Encoder_InitTypeDef enc = {0};
    htim3.Instance        = TIM3;
    htim3.Init.Period     = 0xFFFF;            // 16-bit 計數器讓它自由迴繞
    enc.EncoderMode       = TIM_ENCODERMODE_TI12; // A/B 雙緣 ×4 解碼
    enc.IC1Filter         = 6;                 // 數位濾波去抖(馬達旁必加)
    enc.IC2Filter         = 6;
    enc.IC1Polarity       = TIM_ICPOLARITY_RISING;
    enc.IC2Polarity       = TIM_ICPOLARITY_RISING;
    HAL_TIM_Encoder_Init(&htim3, &enc);
    HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}

20.3 第二步:100Hz 控制任務(回授真正發生的地方)

#define DT            0.01f                    // 100Hz 控制週期
#define CPR           4096.0f                  // counts/圈(§19.3)
#define RAD_PER_CNT   (2.0f * PI / CPR)

static uint16_t last_cnt;
static float    integ;                         // PID 積分項

void ControlTask_100Hz(void)                   // 由 FreeRTOS 週期任務或定時中斷呼叫
{
    /* ① 讀增量:int16_t 強制轉型自動處理 0xFFFF→0 的迴繞 */
    uint16_t now   = __HAL_TIM_GET_COUNTER(&htim3);
    int16_t  delta = (int16_t)(now - last_cnt); // 兩次讀數之差 = 這 10ms 轉的 counts
    last_cnt = now;

    /* ② counts → 實測輪速 (rad/s):這就是「回授」本身 */
    float w_meas = delta * RAD_PER_CNT / DT;

    /* ③ PID:讓實測追上目標(目標來自上位機指令的運動學逆解) */
    float err = w_target - w_meas;
    integ += err * DT;
    integ  = CLAMP(integ, -INTEG_MAX, INTEG_MAX);   // 抗 windup(§7.3)
    float u = KP * err + KI * integ;                // 底盤 PI 就夠

    /* ④ 輸出:經 CAN 下給 FOC 驅動器(或自驅時換算 PWM)*/
    Driver_SetTorque(LEFT, u);

    /* ⑤ 同一份 delta 順便餵 odometry(§4.1 公式)*/
    odom_accumulate(delta /*left*/, delta_right);
}

逐行對應前面的概念:① 是 §19 的硬體計數收割;② 把「格數」換成物理量(§7.2 的單位換算鏈);③ 是 §7 說的「PID 追目標」;⑤ 是同一顆 encoder 的第二重身分。

20.4 兩個容易踩的坑

  1. 迴繞處理:(int16_t)(now - last_cnt) 這個慣用法,靠 16-bit 無號減法的模運算特性,即使 CNT 從 65535 跳回 0 也算出正確增量——前提是兩次讀數間轉動不超過 ±32767 counts(100Hz 下絕對夠)。寫成 if (now < last) ... 的手工判斷反而容易錯。
  2. 週期一致性:DT 必須真的是 10ms。若控制任務被高優先級中斷拖延,實測速度會抖、PID 跟著抖——這就是 §4.2 說「控制環必須固定週期」的具體原因。用硬體定時器觸發、或 FreeRTOS vTaskDelayUntil()(不是 vTaskDelay())保證節拍。

注意邊界:本專案建議架構(現成 FOC 驅動器)下,「速度環在驅動器裡、encoder 接驅動器」,STM32 收到的是 CAN 回報的轉速/位置——上面 ②③ 發生在驅動器內部,STM32 只做 ①⑤ 的 odometry 部分與更外層的邏輯。若自驅(§11.2 第二種情境),這段程式就完整跑在 STM32 上。理解這段程式,兩種架構都看得懂。