在嵌入式系統開發中,利用現有控制器和介面可以有效簡化開發流程。本文介紹瞭如何在 Raspberry Pi 上使用 Wii Nunchuk 控制器作為輸入裝置,並結合 Linux 輸入子系統和 SPI 介面實作互動控制。透過 Python 應用程式,我們可以讀取 Nunchuk 的按鍵和搖桿資料,並將其用於控制 LED 的開關和亮度。此外,文章也詳細說明瞭 Linux SPI 子系統的軟體架構,包括 SPI 匯流排核心、控制器驅動程式和協定驅動程式,並提供了程式碼範例,展示瞭如何在 Linux 系統中使用 SPI 介面進行資料傳輸和硬體控制。這些實作案例有助於理解 Linux 輸入子系統和 SPI 介面的工作原理,並為嵌入式系統的互動控制提供實用的參考。
Wii Nunchuk 控制器在 Raspberry Pi 上的應用:輸入子系統與 LED 控制
在前面的章節中,我們已經成功地在 Raspberry Pi 上建置並佈署了修改後的 kernel module,以支援 Wii Nunchuk 控制器。本章節將探討如何利用 Python 程式設計語言,透過輸入子系統讀取 Nunchuk 的按鍵和搖桿事件,並控制連線至 Raspberry Pi GPIO 的 LED。
joystick_led.py 應用程式開發
首先,我們需要建立一個名為 joystick_led.py 的 Python 應用程式,並將其存放在之前建立的 python_apps 資料夾中。該程式的主要功能是讀取 Nunchuk 的 BTN_C 按鍵事件和 ABS_X 搖桿事件,以控制 LED 的開關和在命令列列印搖桿的 X 軸值。
程式碼解析
from evdev import InputDevice, categorize, ecodes, KeyEvent
from gpiozero import LED
import sys
def main():
joystick = InputDevice('/dev/input/event0')
print(joystick)
for event in joystick.read_loop():
if event.type == ecodes.EV_KEY:
keyevent = categorize(event)
if keyevent.keycode == 'BTN_C':
if keyevent.keystate == KeyEvent.key_down:
led.on()
elif keyevent.keystate == KeyEvent.key_up:
led.off()
elif event.type == ecodes.EV_ABS:
absevent = categorize(event)
if ecodes.bytype[absevent.event.type][absevent.event.code] == 'ABS_X':
if absevent.event.value > 126:
print('right')
print(absevent.event.value)
elif absevent.event.value < 126:
print('left')
print(absevent.event.value)
elif absevent.event.value == 126:
print('centered')
print(absevent.event.value)
if __name__ == '__main__':
try:
led = LED(17) # 將 LED 連線至 GPIO17
main()
except (KeyboardInterrupt, EOFError):
ret = 0
led.close()
sys.exit(ret)
#### 內容解密:
from evdev import InputDevice, categorize, ecodes, KeyEvent:匯入必要的模組以便讀取輸入裝置的事件。joystick = InputDevice('/dev/input/event0'):初始化 Nunchuk 裝置,假設其對應的事件檔案為/dev/input/event0。for event in joystick.read_loop()::進入無限迴圈,不斷讀取 Nunchuk 的事件。if event.type == ecodes.EV_KEY::檢查事件型別是否為按鍵事件(EV_KEY)。if keyevent.keycode == 'BTN_C'::檢查按鍵事件是否為BTN_C按鍵。- 當按下
BTN_C時,led.on()開啟 LED。 - 當釋放
BTN_C時,led.off()關閉 LED。
- 當按下
elif event.type == ecodes.EV_ABS::檢查事件型別是否為絕對軸事件(EV_ABS),通常由搖桿的移動觸發。if ecodes.bytype[absevent.event.type][absevent.event.code] == 'ABS_X'::檢查絕對軸事件是否為 X 軸的移動。- 根據 X 軸的值,列印出搖桿的方向(左、右或置中)及對應的值。
示範與結果
- 載入 nunchuk module:執行
insmod nunchuk.ko以載入 kernel module,讓系統能夠識別 Nunchuk 裝置。 - 執行 joystick_led.py:執行
python3 joystick_led.py以啟動應用程式。此時,按下 Nunchuk 的 C 按鈕會控制 LED 的開關,而移動 X 軸搖桿則會在命令列顯示搖桿的方向和值。
透過本章節的實作,我們成功地將 Wii Nunchuk 控制器與 Raspberry Pi 結合,利用輸入子系統讀取 Nunchuk 的事件,並以 Python 程式控制外部 LED。這不僅展現了 Linux 輸入子系統的強大功能,也為嵌入式系統的互動控制提供了實用的範例。未來,我們可以進一步擴充套件這個專案,將更多的感測器或執行器整合至系統中,創造更豐富的互動體驗。
輸入子系統實驗:開發三個Python應用程式控制Wii Nunchuk
本章節將透過開發三個Python應用程式,示範如何使用Linux輸入子系統讀取Wii Nunchuk的資料,並控制LED亮度及使用Pygame顯示搖桿資料。
joystick_pwm.py應用程式
第一個應用程式joystick_pwm.py將讀取Nunchuk搖桿的X軸資料,並用於改變PWM訊號的佔空比,從而控制LED的亮度。
程式碼實作
from evdev import InputDevice, categorize, ecodes
import RPi.GPIO as GPIO
import time
import sys
import math
def main():
# 初始化輸入裝置
joystick = InputDevice('/dev/input/event0')
print(joystick)
# 進入無限迴圈讀取搖桿事件
for event in joystick.read_loop():
if event.type == ecodes.EV_ABS:
absevent = categorize(event)
if ecodes.bytype[absevent.event.type][absevent.event.code] == 'ABS_X':
# 將X軸的值轉換為佔空比
duty = math.floor((absevent.event.value * 100) / 255)
print(duty)
pwm.ChangeDutyCycle(duty) # 改變PWM佔空比
time.sleep(0.01) # 延遲10毫秒
if __name__ == '__main__':
try:
led_pin = 12 # 連線紅色LED到GPIO18
GPIO.setwarnings(False) # 停用警告
GPIO.setmode(GPIO.BOARD) # 設定針腳編號系統
GPIO.setup(led_pin, GPIO.OUT)
pwm = GPIO.PWM(led_pin, 1000) # 建立PWM例項,頻率1000Hz
pwm.start(0) # 啟動PWM,初始佔空比0%
main()
except (KeyboardInterrupt, EOFError):
pwm.stop()
GPIO.cleanup()
sys.exit(0)
#### 內容解密:
- 輸入裝置初始化:使用
InputDevice('/dev/input/event0')初始化Wii Nunchuk搖桿,假設其在系統中被識別為/dev/input/event0。 - 事件迴圈:使用
read_loop()方法進入無限迴圈讀取搖桿事件。 - 事件處理:過濾出
EV_ABS型別的事件,並檢查是否為ABS_X事件碼,以讀取X軸的值。 - 佔空比計算:將X軸的值(範圍0-255)對映到佔空比(範圍0-100),用於控制LED亮度。
- PWM控制:使用RPi.GPIO函式庫控制GPIO18上的LED亮度,根據佔空比調整PWM訊號。
joystick_pygame.py應用程式
第二個應用程式joystick_pygame.py使用Pygame函式庫顯示Nunchuk搖桿的X-Y軸資料及C、Z按鈕狀態。
程式碼實作
import pygame
from pygame.locals import *
def main():
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
pygame.joystick.init()
try:
joystick = pygame.joystick.Joystick(0)
joystick.init()
except pygame.error:
print("未偵測到搖桿裝置")
return
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((255, 255, 255)) # 清除螢幕
axes = joystick.get_numaxes()
for i in range(axes):
axis = joystick.get_axis(i)
print(f"軸{i}的值:{axis:.3f}")
buttons = joystick.get_numbuttons()
for i in range(buttons):
button = joystick.get_button(i)
print(f"按鈕{i}的值:{button}")
pygame.display.flip()
clock.tick(60)
pygame.quit()
if __name__ == "__main__":
main()
#### 內容解密:
- Pygame初始化:初始化Pygame並設定視窗大小。
- 搖桿初始化:初始化第一個偵測到的搖桿裝置。
- 主迴圈:進入主迴圈處理事件、更新搖桿狀態並顯示在螢幕上。
- 搖桿資料讀取:讀取搖桿的軸值和按鈕狀態,並列印到控制檯。
- 螢幕更新:使用
pygame.display.flip()更新螢幕顯示,並限制迴圈執行頻率在60 FPS。
使用Linux與SPI介面
Serial Peripheral Interface(SPI)是一種同步的四線串列連線,用於連線微處理器到感測器、記憶體和外設。它是一種簡單的「事實上」標準,不夠複雜以至於不需要標準化組織。SPI使用主從組態。
SPI基礎架構
- 三條訊號線包含時鐘(SCK,通常在1-20 MHz範圍內)和平行資料線,分別是「主出從入」(MOSI)和「主入從出」(MISO)訊號。
- 有四種時鐘模式用於交換資料;其中模式0和模式3最為常見。
- 每個時鐘週期都會移位輸出和輸入資料;時鐘只在有資料位元需要移位時才會迴圈。
- 不是所有資料位元都被使用;並非所有通訊協定都使用全雙工功能。
SPI主從裝置互動
- SPI主裝置使用「晶片選擇」線來啟動特定的SPI從裝置,因此這三條訊號線可以平行連線到多個晶片。
- 所有SPI從裝置都支援晶片選擇;這些訊號通常是低電平有效,標記為nCSx(例如nCS0)。
- 一些裝置具有其他訊號,經常包括對主裝置的中斷。
程式介面架構
程式介面由兩種驅動程式組成:控制器驅動程式和通訊協定驅動程式。
- 控制器驅動程式支援SPI主控制器,控制時鐘和晶片選擇,移位資料位元,並組態基本的SPI特性,如時脈頻率和模式。
- 通訊協定驅動程式支援特定SPI從裝置的功能,根據訊息,並依賴控制器驅動程式來控制SPI主硬體。
範例程式碼:使用SPI進行資料傳輸
struct spi_transfer t[2];
struct spi_message m;
spi_message_init(&m);
memset(t, 0, sizeof t);
t[0].tx_buf = command;
t[0].len = at25->addrlen + 1;
spi_message_add_tail(&t[0], &m);
t[1].rx_buf = buf;
t[1].len = count;
spi_message_add_tail(&t[1], &m);
status = spi_sync(at25->spi, &m);
內容解密:
- 初始化一個SPI訊息結構
m,並清空傳輸結構t。 - 設定第一個傳輸結構
t[0]的傳送緩衝區為command,長度為at25->addrlen + 1,並將其加入到訊息佇列中。 - 設定第二個傳輸結構
t[1]的接收緩衝區為buf,長度為count,並將其加入到訊息佇列中。 - 使用
spi_sync函式同步傳送訊息,完成資料的傳輸。
SPI基本I/O原語
spi_async():基本的非同步I/O原語,可以在任何上下文中發出請求,並透過回呼函式報告完成情況。spi_sync():同步的包裝函式,只能在可睡眠的上下文中使用。spi_write_then_read()及其包裝函式:用於小量資料的讀寫操作,例如spi_w8r16()包裝函式,用於寫入8位元命令並讀取16位元回應。
範例程式碼:spi_w8r16函式
static inline ssize_t spi_w8r16(struct spi_device *spi, u8 cmd)
{
ssize_t status;
u16 result;
status = spi_write_then_read(spi, &cmd, 1, &result, 2);
/* return negative errno or unsigned value */
return (status < 0) ? status : result;
}
內容解密:
- 定義一個行內函式
spi_w8r16,用於向SPI裝置寫入一個8位元命令並讀取16位元回應。 - 使用
spi_write_then_read函式完成寫入和讀取操作。 - 根據操作狀態傳回負的錯誤碼或無符號的結果值。
Linux SPI 子系統詳解
Linux SPI 子系統是根據 Linux 裝置模型構建的,主要由多個驅動程式組成,包括 SPI 匯流排核心、SPI 控制器的驅動程式以及 SPI 協定驅動程式。
SPI 匯流排核心
SPI 匯流排核心位於 drivers/spi/ 目錄下的 spi.c 檔案中。它提供了一組介面,用於支援 SPI 客戶端驅動程式與 SPI 匯流排主控器之間的互動。SPI 匯流排核心透過 bus_register() 函式向核心註冊,並定義了一個名為 spi_bus_type 的 bus_type 結構。
struct bus_type spi_bus_type = {
.name = "spi",
.dev_groups = spi_dev_groups,
.match = spi_match_device,
.uevent = spi_uevent,
};
EXPORT_SYMBOL_GPL(spi_bus_type);
內容解密:
.name = "spi":定義了 SPI 匯流排的名稱。.dev_groups = spi_dev_groups:指定了與 SPI 裝置相關的屬性群組。.match = spi_match_device:用於匹配 SPI 裝置與驅動程式的函式。.uevent = spi_uevent:處理與 SPI 裝置相關的 uevent 事件的函式。
SPI 控制器的驅動程式
SPI 控制器的驅動程式位於核心原始碼樹的 drivers/spi/ 目錄下。SPI 控制器是一個平台裝置(在裝置樹中宣告),必須透過 of_platform_populate() 函式註冊到平台匯流排,並使用 module_platform_driver() 函式註冊到 SPI 匯流排核心。
static struct platform_driver bcm2835_spi_driver = {
.driver = {
.name = DRV_NAME,
.of_match_table = bcm2835_spi_match,
},
.probe = bcm2835_spi_probe,
.remove = bcm2835_spi_remove,
};
module_platform_driver(bcm2835_spi_driver);
內容解密:
.name = DRV_NAME:定義了驅動程式的名稱。.of_match_table = bcm2835_spi_match:指定了與裝置樹匹配的表格。.probe = bcm2835_spi_probe:當裝置被探測到時呼叫的函式。.remove = bcm2835_spi_remove:當裝置被移除時呼叫的函式。
SPI 協定驅動程式
SPI 協定驅動程式位於 Linux 驅動程式目錄下的不同子目錄中,取決於裝置型別(例如,輸入裝置的驅動程式位於 linux/drivers/input/)。驅動程式碼特定於裝置(如加速計、數模轉換器等),並使用 SPI 核心 API 與 SPI 主控器驅動程式進行通訊,傳送/接收資料到/從 SPI 裝置。
BCM2835 SPI 控制器驅動程式範例
在 drivers/spi/spi-bcm2835.c 檔案中,可以看到 BCM2835 SPI 控制器的初始化和註冊過程。
static int bcm2835_spi_probe(struct platform_device *pdev)
{
struct spi_controller *ctlr;
struct bcm2835_spi *bs;
int err;
ctlr = devm_spi_alloc_master(&pdev->dev, ALIGN(sizeof(*bs), dma_get_cache_alignment()));
if (!ctlr)
return -ENOMEM;
// ... 省略部分程式碼 ...
err = spi_register_controller(ctlr);
if (err) {
dev_err(&pdev->dev, "could not register SPI controller: %d\n", err);
goto out_dma_release;
}
return 0;
out_dma_release:
bcm2835_dma_release(ctlr, bs);
clk_disable_unprepare(bs->clk);
return err;
}
內容解密:
devm_spi_alloc_master():分配一個 SPI 主控器結構。spi_register_controller(ctlr):註冊 SPI 控制器到 SPI 匯流排核心。bcm2835_dma_release(ctlr, bs):釋放 DMA 資源。clk_disable_unprepare(bs->clk):關閉並取消準備時鐘。