RP2040 PIO UART(TX) 實現綜合總結
1. UART 基本原理
info
UART(通用非同步收發傳輸器)是一種常用的串行通信協議。在我們的實現中:
- 使用 8 數據位,1 起始位,1 停止位(8N1 格式)
- 不使用流控制和奇偶校驗
- 預設波特率設置為 9600 bps
2. PIO 程序結構
2.1 UART TX PIO 程序
uart_tx.pio
.program uart_tx
.side_set 1 opt
pull side 1 [7] ; 從 FIFO 獲取數據,設置停止位(高電平)
set x, 7 side 0 [7] ; 設置循環計數器,發送起始位(低電平)
bitloop:
out pins, 1 ; 發送一位數據
jmp x-- bitloop [6] ; 循環直到發送完 8 位數據
關鍵點
.side_set 1 opt: 定義一個可選的 1 位側設,用於控制 TX 引腳pull: 從 TX FIFO 獲取 32 位數據set x, 7: 設置循環計數器為 7(8位數據)out pins, 1: 輸出一位數據jmp x-- bitloop [6]: 循環發送 8 位數據,每次迭代 8 個週期
2.2 FIFO 使用和字符存儲
FIFO 基本容量和合併
- 每個方向(TX 或 RX)的 FIFO 可以存儲 4 個 32 位字。
- 1 個 32 位字理論上可存儲 4 個 char(每個 char 8 位)。
- TX 和 RX FIFO 合併後,深度增加到 8 個 32 位字。
- 理論上合併後可存儲 32 個 char。
實際使用中的 char 存儲
- 典型 UART 實現中,通常每次只放入一個 char(8 位)到 FIFO。
- PIO 程序每次從 FIFO 取出 32 位數據,但通常只使用其中的 8 位。
FIFO 填充示例("ABCD")
32位FIFO條目1: [A] [ ] [ ] [ ] (只有 'A' 被存儲,其餘未使用)
32位FIFO條目2: [B] [ ] [ ] [ ] (只有 'B' 被存儲)
32位FIFO條目3: [C] [ ] [ ] [ ] (只有 'C' 被存儲)
32位FIFO條目4: [D] [ ] [ ] [ ] (只有 'D' 被存儲)
FIFO 使用策略
- 通常逐個 char 填充 FIFO,簡化 PIO 程序邏輯。
- PIO 程序設計為在 FIFO 有數據時立即處理,不等待 FIFO 填滿。
2.3 時序控制
- 每個 UART 位使用 8 個 PIO 時鐘週期
- 使用指令後的延遲(如
[7])來精確控制時序 - 總幀長度:1(起始位)+ 8(數據位)+ 1(停止位)= 10 位 * 8 週期 = 80 PIO 時鐘週期/幀
3. PIO 初始化和配置
uart_tx_init.c
static inline void uart_tx_program_init(PIO pio, uint sm, uint offset, uint pin_tx, uint baud) {
pio_sm_config c = uart_tx_program_get_default_config(offset);
sm_config_set_sideset_pins(&c, pin_tx);
sm_config_set_out_pins(&c, pin_tx, 1);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
float div = (float)clock_get_hz(clk_sys) / (8 * baud);
sm_config_set_clkdiv(&c, div);
pio_gpio_init(pio, pin_tx);
pio_sm_set_consecutive_pindirs(pio, sm, pin_tx, 1, true);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
關鍵配置步驟
- 設置側設(side-set)和輸出引腳
- 配置 FIFO 為僅 TX 模式
- 計算並設置時鐘分頻以匹配目標波特率
- 初始化 GPIO 和設置引腳方向
- 初始化和啟用狀態機
4. FIFO 使用和管理
FIFO 操作示例
static inline void uart_tx_program_putc(PIO pio, uint sm, char c) {
pio_sm_put_blocking(pio, sm, (uint32_t)c);
}
static inline void uart_tx_program_puts(PIO pio, uint sm, const char *s) {
while (*s) {
uart_tx_program_putc(pio, sm, *s++);
}
}
5. 主程序實現
main.c
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "uart_tx.pio.h"
int main() {
const uint PIN_TX = 0;
const uint SERIAL_BAUD = 9600;
PIO pio = pio0;
uint sm = 0;
uint offset = pio_add_program(pio, &uart_tx_program);
uart_tx_program_init(pio, sm, offset, PIN_TX, SERIAL_BAUD);
while (true) {
uart_tx_program_puts(pio, sm, "Hello, world! (from PIO!)\n");
sleep_ms(1000);
}
}
主程序步驟
- 選擇 PIO 實例和狀態機
- 將 PIO 程序加載到 PIO 指令內存
- 初始化 UART TX 程序
- 在主循環中發送數據
6. 波特率計算和精度
實際波特率計算
實際波特率 = (系統時鐘頻率) / (時鐘分頻 * 每幀週期數) = 125,000,000 / (div * 80)
其中,div 是初始化時計算的分頻值。
注意:實際波特率可能與目標波特率略有偏差,這取決於系統時鐘頻率和可用的分頻值。
7. 8 位 UART 傳輸詳解:發送 "ABC"
7.1 UART 幀結構(8N1 格式)
每個 UART 字符幀包含 10 位:
- 1 個起始位(始終為 0)
- 8 個數據位(最低有效位 LSB 先發送)
- 1 個停止位(始終為 1)
7.2 字符 "ABC" 的二進制表示
- 'A': ASCII 65 = 0100 0001
- 'B': ASCII 66 = 0100 0010
- 'C': ASCII 67 = 0100 0011
7.3 發送過程詳解
字符 'A' 的發送過程
完整幀:
0 01000001 1
↑ ↑ ↑
| | |
| | 停止位
| 數據位
起始位
PIO 程序執行過程:
-
pull side 1 [7]:- 從 FIFO 獲取 'A'(0x41)
- TX 引腳保持高電平(停止位狀態)
-
set x, 7 side 0 [7]:- 設置循環計數器 x = 7
- 發送起始位(低電平)
-
bitloop:循環 8 次:out pins, 1: 發送一位數據jmp x-- bitloop [6]: 減少計數器並跳轉- 發送順序:1, 0, 0, 0, 0, 0, 1, 0 (LSB first)
-
循環結束後,通過
side 1設置停止位(高電平)
字符 'B' 和 'C' 的發送
'B' 和 'C' 的發送過程與 'A' 相同,只是數據位不同:
- 'B': 0 01000010 1
- 'C': 0 01000011 1
7.4 FIFO 操作
當調用 uart_tx_program_puts(pio, sm, "ABC") 時:
- 'A' 被放入 FIFO
- PIO 程序從 FIFO 拉取 'A' 並開始發送
- 同時,'B' 被放入 FIFO
- 'A' 發送完成後,PIO 程序立即拉取並發送 'B'
- 'C' 被放入 FIFO
- 'B' 發送完成後,PIO 程序拉取並發送 'C'
7.5 時序分析
假設 UART 波特率為 9600 bps:
- 每個位持續時間:1/9600 秒 ≈ 104.17 微秒
- 每個字符(10 位)傳輸時間:104.17 * 10 ≈ 1.0417 毫秒
- "ABC" 總傳輸時間:3 * 1.0417 ≈ 3.125 毫秒
8. 高級考慮因素
-
錯誤處理:
- PIO 不提供內置的幀錯誤檢測
- 可以通過額外的 PIO 程序邏輯或軟件層面實現錯誤檢測
-
雙向通信:
- 需要單獨的 RX PIO 程序
- 可能需要使用額外的狀態機或 PIO 實例
-
DMA 使用:
- 對於大量數據傳輸,可以配合 DMA 使 用
- DMA 可以自動填充 TX FIFO,減少 CPU 干預
-
資源利用:
- 一個 UART 實例佔用一個 PIO 狀態機
- 需要考慮 PIO 指令內存的使用
-
靈活性:
- 可以輕鬆修改 PIO 程序以支持不同的數據格式(如 7 位數據、2 個停止位)
- 可以實現自定義協議或非標準波特率
9. 調試技巧
調試方法
- 使用邏輯分析儀或示波器驗證信號時序
- 利用
printf調試輸出來監控數據流 - 使用 Pico 的 LED 進行簡單的可視化調試
- 在關鍵點添加 GPIO 觸發,方便使用示波器捕獲特定事件
10. 性能考慮
- PIO UART 實現允許高速數據傳輸,僅受 PIO 時鐘頻率限制
- 對於簡單的數據發送,PIO 方法可能比硬件 UART 更靈活
- 對於複雜的協議或需要頻繁更改配置的場景,PIO 方法尤其有優勢
11. 未來擴展
可能的擴展方向
- 實現 UART 接收功能
- 添加流控制支持
- 實現奇偶校驗
- 支持可變數據位數(5-8 位)和停止位數
- 實現自動波特率檢測
完整代碼
main.cpp
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "uart_tx.pio.h"
int main() {
// We're going to use PIO to print "Hello, world!" on the same GPIO which we
// normally attach UART0 to.
const uint PIN_TX = 7;
// This is the same as the default UART baud rate on Pico
const uint SERIAL_BAUD = 9600;
PIO pio = pio0;
uint sm = 0;
uint offset = pio_add_program(pio, &uart_tx_program);
uart_tx_program_init(pio, sm, offset, PIN_TX, SERIAL_BAUD);
while (true) {
uart_tx_program_puts(pio, sm, "Hello, world! (from PIO!)\n");
sleep_ms(1000);
}
}
uart_tx.pio
;
; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;
.program uart_tx
.side_set 1 opt
; An 8n1 UART transmit program.
; OUT pin 0 and side-set pin 0 are both mapped to UART TX pin.
pull side 1 [7] ; Assert stop bit, or stall with line in idle state
set x, 7 side 0 [7] ; Preload bit counter, assert start bit for 8 clocks
bitloop: ; This loop will run 8 times (8n1 UART)
out pins, 1 ; Shift 1 bit from OSR to the first OUT pin
jmp x-- bitloop [6] ; Each loop iteration is 8 cycles.
% c-sdk {
#include "hardware/clocks.h"
static inline void uart_tx_program_init(PIO pio, uint sm, uint offset, uint pin_tx, uint baud) {
// Tell PIO to initially drive output-high on the selected pin, then map PIO
// onto that pin with the IO muxes.
pio_sm_set_pins_with_mask(pio, sm, 1u << pin_tx, 1u << pin_tx);
pio_sm_set_pindirs_with_mask(pio, sm, 1u << pin_tx, 1u << pin_tx);
pio_gpio_init(pio, pin_tx);
pio_sm_config c = uart_tx_program_get_default_config(offset);
// OUT shifts to right, no autopull
sm_config_set_out_shift(&c, true, false, 32);
// We are mapping both OUT and side-set to the same pin, because sometimes
// we need to assert user data onto the pin (with OUT) and sometimes
// assert constant values (start/stop bit)
sm_config_set_out_pins(&c, pin_tx, 1);
sm_config_set_sideset_pins(&c, pin_tx);
// We only need TX, so get an 8-deep FIFO!
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
// SM transmits 1 bit per 8 execution cycles.
float div = (float)clock_get_hz(clk_sys) / (8 * baud);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
static inline void uart_tx_program_putc(PIO pio, uint sm, char c) {
pio_sm_put_blocking(pio, sm, (uint32_t)c);
}
static inline void uart_tx_program_puts(PIO pio, uint sm, const char *s) {
while (*s)
uart_tx_program_putc(pio, sm, *s++);
}
%}