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

核心

robot-notes /核心/硬體/數位電路

數位電路:Open-drain 與 GPIO 輸出形式

要看懂 I²C 接線、急停訊號、霍爾輸入為什麼要上拉電阻,得先懂數位輸出的兩種電路形式:推挽(push-pull)與開漏(open-drain)。本篇從電晶體層解釋兩者差異,以及 STM32 在程式上怎麼設定。

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


13. Open-drain 是什麼:數位輸出的兩種電路形式

13.1 先看「正常」的輸出:推挽 (push-pull)

數位晶片要輸出 0 或 1,腳位內部其實是兩顆電晶體,一顆負責「推高」、一顆負責「拉低」:

推挽 vs open-drain 對照:推挽兩態都有力氣;open-drain 拿掉上管,輸出 1 時腳位浮空

兩種狀態都「有力氣」——高就是實實在在的 VCC,低就是實實在在的 GND。STM32 GPIO 預設就是這種。

13.2 Open-drain:把上管拿掉

Open-drain(開漏;BJT 版本叫 open-collector 開集)= 只有下管,沒有上管(見上圖右側):

「drain」就是那顆 MOSFET 下管的汲極,「open」= 它沒接負載、開放著,等外部電路決定。所以:

13.3 上拉電阻:幫它補上「高」的力氣

解法就是外接一顆電阻到電源(上拉電阻 pull-up):

上拉電阻接線:3.3V 經 10kΩ 接到輸出腳與接收端,下管導通讀到 0、下管關閉靠電阻讀到 1

這就是 §11.3「霍爾線必須上拉,否則永遠讀到不定值」的完整原因:霍爾開關內部只有那顆下管。

電阻值的取捨:太大(100kΩ)→ 拉高速度慢(跟線路電容形成 RC 延遲)、抗噪差;太小(1kΩ)→ 每次拉低都流較大電流、耗電。4.7k–10kΩ 是常用甜蜜點

13.4 為什麼要「故意」設計成 open-drain?三個經典理由

  1. 電平轉換免費送:輸出高電平由「上拉接到哪」決定,跟晶片自己的供電無關。5V 供電的霍爾開關,上拉到 3.3V,輸出擺幅就是 0–3.3V——直接安全接 STM32,不用電平轉換晶片。這正是 §11.3 接線圖把上拉接 3.3V 的原因。
  2. 多裝置共享一條線不打架 (wired-AND):推挽輸出不能並聯——一顆推高、一顆拉低 = 短路。open-drain 並聯沒事:誰都只能拉低,「任一裝置拉低 → 線就是低」。I²C 匯流排、多裝置共享的中斷線/故障線都靠這個特性。
  3. 故障安全語意:斷線、裝置斷電時,線被上拉電阻維持在「高」——所以慣例把「高 = 正常、低 = 警報」,斷線會自然表現為可偵測的異常而不是假正常。

理由 2 的 wired-AND 是一個跨章節的根本機制,值得一張圖記住——I²C、CAN 仲裁、共享中斷/故障線都站在它上面:(嚴格說,「全放手才高」要在「高=邏輯 1、低=邏輯 0」的約定下才等於邏輯 AND,故名 wired-AND;若反過來採低有效約定,同一條線物理不變、邏輯上就是 §15.2 講的 wired-OR。)

wired-AND:全部放手→上拉到高;任一裝置拉低→整條線低

13.5 STM32 端的對應設定

情境 GPIO 模式
讀霍爾(外部已加上拉電阻) Input(浮空即可)
讀霍爾(板上沒空間放電阻) Input + 內部上拉(STM32 內建 ~40kΩ 弱上拉,訊號乾淨時可頂用;馬達旁噪聲環境建議仍用外部 10kΩ 強上拉)
STM32 自己當 open-drain 輸出(如 I²C) Output open-drain + 外部上拉

延伸:§6 的 CAN 匯流排「顯性/隱性」電平機制,本質上就是同一個 wired-AND 思想的差分版——任何節點都能把匯流排打成顯性(0),沒人發送時回到隱性(1),這是 CAN 硬體仲裁(ID 小者勝出)的物理基礎。


14. STM32 的 open-drain 程式控制

14.1 硬體背景:每支 GPIO 都內建兩種輸出電路

STM32 每支 GPIO 腳位內部同時做了推挽和開漏電路,用暫存器選擇用哪種。由三個暫存器決定行為:

暫存器 作用 對應 HAL 欄位
MODER 模式:輸入 / 輸出 / 複用功能(AF) / 類比 .Mode 的 INPUT/OUTPUT/AF
OTYPER 輸出型式:0 = 推挽 (PP),1 = 開漏 (OD) .Mode_PP / _OD 後綴
PUPDR 內部上拉 / 下拉 / 無 .Pull

HAL 把前兩者合併成一個 .Mode 常數,命名規則一看就懂:

GPIO_MODE_OUTPUT_PP   一般輸出,推挽
GPIO_MODE_OUTPUT_OD   一般輸出,開漏      ← 軟體直接控制的 open-drain
GPIO_MODE_AF_PP       交給外設,推挽      (如 TIM PWM、USART)
GPIO_MODE_AF_OD       交給外設,開漏      (如 I²C 的 SCL/SDA)
GPIO_MODE_INPUT       輸入(讀霍爾用這個)

14.2 場景一:讀霍爾訊號(輸入 + 上拉)

open-drain 是「對方」(霍爾開關),STM32 只是讀;要控制的是上拉:

GPIO_InitTypeDef g = {0};
g.Pin  = GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8;   // PB6/7/8 = HA/HB/HC
g.Mode = GPIO_MODE_INPUT;
g.Pull = GPIO_PULLUP;        // 內部 ~40kΩ 弱上拉;板上有外部 10kΩ 時設 GPIO_NOPULL
HAL_GPIO_Init(GPIOB, &g);

uint8_t hall = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6) << 2)
             | (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) << 1)
             |  HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8);   // 組成 §11 的 3-bit 狀態

14.3 場景二:STM32 自己當 open-drain 輸出

GPIO_InitTypeDef g = {0};
g.Pin   = GPIO_PIN_5;
g.Mode  = GPIO_MODE_OUTPUT_OD;     // 開漏輸出
g.Pull  = GPIO_NOPULL;             // 通常靠外部上拉電阻
g.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &g);

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 寫 0 → 下管導通,拉低(有力氣)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);   // 寫 1 → 下管關閉,「放手」

關鍵語意差異:寫的 API 跟推挽一模一樣,但「寫 1」的意思變了——

  推挽寫 1 開漏寫 1
實際動作 主動推到 VCC 只是放手 (release),電平由外部上拉決定
腳位電壓 一定是 3.3V 上拉接 5V 就是 5V,沒上拉就浮空

兩個實用推論:

  1. 電平轉換:F4 多數腳位是 5V-tolerant(查 datasheet 的 FT 標記),設成 OD + 外部上拉到 5V,就能輸出 0–5V 訊號給 5V 裝置——軟體一行不用改。
  2. 讀回驗證:開漏腳位寫 1 後讀 IDR(輸入暫存器),若讀到 0 表示線上有別人在拉低——I²C 仲裁與匯流排卡死偵測就用這招。

14.4 場景三:交給外設的 open-drain(I²C 接 IMU)

I²C 協議規定 SCL/SDA 必須開漏(§13.4 的 wired-AND:多裝置共享、slave 可以 clock-stretching 拉住時脈)。腳位設 GPIO_MODE_AF_OD 後,控制權交給 I2C 外設,程式裡操作的是 I²C API 而不是 GPIO:

g.Pin       = GPIO_PIN_8 | GPIO_PIN_9;   // PB8=SCL, PB9=SDA
g.Mode      = GPIO_MODE_AF_OD;           // 複用功能 + 開漏(I²C 硬性要求)
g.Pull      = GPIO_NOPULL;               // I²C 一定用外部上拉(2.2k–4.7kΩ)
g.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &g);
// 之後用 HAL_I2C_Mem_Read() 等 API 讀 IMU,GPIO 層不再碰

常見錯誤:把 I²C 腳位誤設成 AF_PP → 兩個裝置對推短路,或 slave 無法拉低 → 表現為「I²C 永遠 timeout / NACK」,硬體看起來都接對了。除錯 I²C 先檢查 OD + 上拉電阻。

14.5 暫存器層(理解 HAL 在做什麼)

GPIOA->MODER  |=  (0b01 << (5*2));   // PA5 設為輸出
GPIOA->OTYPER |=  (1 << 5);          // 1 = open-drain(0 = 推挽)
GPIOA->BSRR    =  (1 << 5);          // 寫 1(放手)
GPIOA->BSRR    =  (1 << (5+16));     // 寫 0(拉低);BSRR 高 16 位是 reset 位

OTYPER 一個 bit 就是推挽/開漏的全部差別——印證 §14.1:兩種電路都在腳位裡,程式只是選用哪一種。


15. 為什麼開漏是「留下管」而不是「留上管」

🔬 進階,初讀可跳過。 這節深入到半導體物理(NMOS/PMOS、載子遷移率),是「為什麼」的補充。只想會用 open-drain 的話,讀完 §13、§14 就夠。結論先給:下管(NMOS)又便宜又好驅動,所以開漏都用它;想知道物理原因再往下讀。

§13 說 open-drain 是把推高的上管拿掉。鏡像的設計——留上管、拿掉下管、外部接下拉電阻——叫 open-source(開源極),物理上完全成立,但邏輯訊號幾乎沒人用。原因有三層。

15.1 電晶體物理:拉低的 NMOS 天生比推高的好做

MOSFET 分 N 通道 (NMOS) 和 P 通道 (PMOS) 兩種:

  NMOS(適合放下面拉低) PMOS(適合放上面推高)
載子 電子,遷移率高 電洞,遷移率約只有電子的 1/2–1/3
同樣導通電阻的晶片面積 大 2–3 倍(= 貴)
閘極驅動 Source 接 GND,給 +V 就開,最簡單 需要相對 VCC 的負向驅動

想用 NMOS 放上面推高?那就回到 §12.4 的坎 2:Gate 要比 Source 高,需要自舉電路——為一支邏輯腳位加 bootstrap 完全不划算。所以「一顆接地的 NMOS 拉低」是最小、最便宜、驅動最簡單、導通電阻最低的開關,開漏設計剛好只需要這一顆。

15.2 系統觀點:GND 是大家共有的,VCC 不是

這是最關鍵的理由。一塊板子上 3.3V、5V、12V 各種電源域並存,但 GND 全系統共用:

多裝置共線的邏輯也一樣:開漏是 wired-AND(任一裝置拉低 → 線為低),配「低 = 事件/警報」的慣例;開源變成 wired-OR 且高電平互推,不同 VCC 時誰推贏都是問題。

15.3 歷史慣性:open-collector 時代就定了

MOSFET 之前的 BJT 時代同樣不對稱:NPN(對應拉低)比 PNP(對應推高)增益高、速度快、便宜,所以 TTL 時代的共享匯流排就是 open-collector(NPN 拉低)+ 上拉。後來的 I²C、SMBus、中斷線、各種感測器輸出全部沿用這個慣例,生態系(上拉電阻的預設、「低有效」的訊號命名 nFAULT/nINT)都建立在它上面。

open-source 並非不存在:汽車電子的高邊開關(high-side switch,負載一端固定接地、由上面開關供電)就是這個結構——但那是「給負載供電」的功率場景,不是邏輯訊號。邏輯訊號的世界裡,開漏 + 上拉是唯一主流。

一句話總結:下管便宜好驅動(物理),GND 全系統共用而 VCC 各自為政(系統),五十年生態沿用(歷史)——三個理由都指向同一個設計。