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,腳位內部其實是兩顆電晶體,一顆負責「推高」、一顆負責「拉低」:
兩種狀態都「有力氣」——高就是實實在在的 VCC,低就是實實在在的 GND。STM32 GPIO 預設就是這種。
13.2 Open-drain:把上管拿掉
Open-drain(開漏;BJT 版本叫 open-collector 開集)= 只有下管,沒有上管(見上圖右側):
「drain」就是那顆 MOSFET 下管的汲極,「open」= 它沒接負載、開放著,等外部電路決定。所以:
- 輸出 0:確實被拉到 GND ✓
- 輸出 1:腳位什麼都不接,像斷掉的電線——讀它會得到隨環境噪聲漂移的不定值 ✗
13.3 上拉電阻:幫它補上「高」的力氣
解法就是外接一顆電阻到電源(上拉電阻 pull-up):
這就是 §11.3「霍爾線必須上拉,否則永遠讀到不定值」的完整原因:霍爾開關內部只有那顆下管。
電阻值的取捨:太大(100kΩ)→ 拉高速度慢(跟線路電容形成 RC 延遲)、抗噪差;太小(1kΩ)→ 每次拉低都流較大電流、耗電。4.7k–10kΩ 是常用甜蜜點。
13.4 為什麼要「故意」設計成 open-drain?三個經典理由
- 電平轉換免費送:輸出高電平由「上拉接到哪」決定,跟晶片自己的供電無關。5V 供電的霍爾開關,上拉到 3.3V,輸出擺幅就是 0–3.3V——直接安全接 STM32,不用電平轉換晶片。這正是 §11.3 接線圖把上拉接 3.3V 的原因。
- 多裝置共享一條線不打架 (wired-AND):推挽輸出不能並聯——一顆推高、一顆拉低 = 短路。open-drain 並聯沒事:誰都只能拉低,「任一裝置拉低 → 線就是低」。I²C 匯流排、多裝置共享的中斷線/故障線都靠這個特性。
- 故障安全語意:斷線、裝置斷電時,線被上拉電阻維持在「高」——所以慣例把「高 = 正常、低 = 警報」,斷線會自然表現為可偵測的異常而不是假正常。
理由 2 的 wired-AND 是一個跨章節的根本機制,值得一張圖記住——I²C、CAN 仲裁、共享中斷/故障線都站在它上面:(嚴格說,「全放手才高」要在「高=邏輯 1、低=邏輯 0」的約定下才等於邏輯 AND,故名 wired-AND;若反過來採低有效約定,同一條線物理不變、邏輯上就是 §15.2 講的 wired-OR。)
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,沒上拉就浮空 |
兩個實用推論:
- 電平轉換:F4 多數腳位是 5V-tolerant(查 datasheet 的 FT 標記),設成 OD + 外部上拉到 5V,就能輸出 0–5V 訊號給 5V 裝置——軟體一行不用改。
- 讀回驗證:開漏腳位寫 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 全系統共用:
- 開漏(拉低):「低」= 接到共同的 GND,任何電源域的晶片看到的「低」都一樣;「高」由上拉電阻接的電壓決定 → 接收方自己選電平,5V 晶片和 3.3V 晶片掛同一條線互不傷害(§13.4 理由 1 的根源)。
- 開源(推高):「高」= 推到發送方自己的 VCC。5V 晶片會把 5V 硬灌給線上所有人,3.3V 接收方直接過壓;不同 VCC 的裝置根本不能共線。電平轉換的好處完全消失,還變成危險。
多裝置共線的邏輯也一樣:開漏是 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 各自為政(系統),五十年生態沿用(歷史)——三個理由都指向同一個設計。