2017年3月24日 星期五

用 AVR atmega16u2 連接 USB


USB 是歷史很久的通訊規格, 市面上已經有多到炸的產品產出
這個規格是設計來熱拔插的, 隨時可以接上或移除
不過可能是因為許多系統都有支援, 速度又夠快
即使 on-board 裝置也是有走 USB 的, 像是 wifi 和藍芽模組
雖然 ARM 的系統很早就有 USB 但以前不一定會用到
現在則是一定會用, 有的 SoC 還內建多個 host
新的手機方案更是加碼到 USB3, 我們搞系統的就必須面對它
我的 WT-13 原先是做 BLE 使用, 不過為了順便玩 USB
因此特別選了顆有 USB 的 MCU: atmega16u2
我們將利用這顆來入門 USB, 學點 USB 基礎概念


要做 USB 首先要看 spec!

http://www.usb.org/developers/docs/usb20_docs/
抓這個:Universal Serial Bus Revision 2.0 specification

另有中文的:
USB (Universal Serial Bus) - 成大資工Wiki

不過我還是建議用戶要翻過原文的規格, 因為不公平不能只有我看過XD
看原文的很吃力, 不過如果要對原始碼還是要看, 資料格式, 數字代號定義
這些都寫在規格裡, 寫程式的時候也會用到, 即使是用現成的軟體
最少也要會填裝置描述吧 (Descriptor), 這些細節都在規格書裡
我看了規格書, 先翻個大概, 然後翻原始碼, 再回去對規格書, 再看原始碼
然後再加上高貴的封包分析儀來回對資料對了好幾次才搞懂這協議在玩什麼
這門檻真高, 花了不少力氣才跨過去, 不敢說能做出什麼裝置
但至少基本的協議應該是通了

這裡先發放源碼: WT-13-usb.zip
電路圖共用前篇 cc2540 的:WT-12-fix.pdf
不過沒有上 cc2540, 只有上 atmega16u2, 晶振改為 16MHz
接線的重點 atmega16u2 datasheet 上有寫, 我是照這個接的:

如果你的設備用不同的電壓或是供電方式需參考 datasheet
比較需要注意的是這顆 MCU 的 PLL 只有固定 6 倍升頻, 輸入頻率只有除二選項
所以只能 8MHz 或 16MHz 二選一, 接其他頻率應該是不會動的 (應該說會認不到)
它沒有複雜的變頻電路, 這一顆沒賣幾個錢就別要求這了, 這種價位還有 USB 很彿心了XD
再加上電壓對頻率的限制, 總共大概就兩個選項, 3.3V 的就接 8MHz 內部 RC
不然就是 5V 接 16MHz 外部晶振, 除非特殊需求不然大概就這兩種配置
其中 5V 16MHz 就是 Arduino UNO R3 面對 USB 那顆的配置
我的程式理論上可以套到 R3 上, 不過我沒試過, 手邊唯一一張 R3 在 CNC 機台上
由於沒需求就懶得買一片來試了

接著編譯軟體, AVR 的 USB 有人做了一個 framework 讓大家使用:
LUFA (Lightweight USB Framework for AVRs, formerly known as MyUSB)
不過那包的範例 16u2 編不過, 我把它下載來修改, 可參考源碼包 lufa 目錄
原始 LUFA 壓縮檔已經在裡面 (lufa-LUFA-151115.zip), 解開來後把 lufa.patch 打上去
這樣可以編過 VirtualSerial 這個範例, 用 usbasp 燒錄, 細節請看 lufa.patch 內容
由於 16u2 沒有 PORTE 這種 IO, 直接註解, 他還有用 PORTB 來收訊號, 我懶得接線
所以我改加入 xxx() 這函數, 計算超過 60000 就印一行字到輸出
編譯後燒錄到 16u2, 把它接上 ubuntu 用 minicom 開啟 ttyACM0
就可以看到每隔約一秒印一行 A=60001 字串
ubuntu 的 ModemManager 會在偵測到 ACM 設備時就去探看能不能用
如果發現接上後開節點失敗印出資源忙碌, 通常就是 ModemManager

usb serial driver and ATE1 E0

我是在掃封包才看到這, 通常等個幾秒就可以用, 如果不想等可以把 ModemManager 幹掉

sudo systemctl stop ModemManager

幹掉就好不建議停用, 不然哪天心血來潮接上 3G 數據機就不會有人幫你建連線了
對 USB 協議沒有興趣的用戶可以用這包去開發
建立 USB 連線後可以用檔案讀寫的方式去收發資料
fprintf 可用, 可以轉十進位數字輸出, 挺屌的
這是 avr-libc 做的:<stdio.h>: Standard IO facilities
這段程式寫在 lufa-LUFA-151115/LUFA/Drivers/USB/Class/Device/CDCClassDevice.c
CDC_Device_CreateStream() 這裡, 給它 putchar 和 getchar 函數指標各一個
產生字串時就幫你用這函數傳出去, 不過我使用在我的程式好像有問題
印象中是印空格時, 不是很確定, 也沒有回去測 lufa
如果遇到問題可以改用 CDC_Device_putchar() 或 CDC_Device_getchar()


如果對協議有興趣可以繼續往下看
接著我借用公司的高檔機器把改的 lufa 範例和 ubuntu 電腦溝通的封包撈出來
這台機器長這樣:

很久的機器了, 據說當年購入價為 20 萬台幣
(更正:查資產後發現應該是 40 萬...還好嘛才多 20 (被巴)XD)
很明顯的一般用戶不會去買這種東西XD 不過還不算買不起的東西
只是有沒有必要的問題, 我只是學習一下, 沒打算靠這吃飯, 借用一下就好
這台蠻不錯的, 不只是封包, 連封包之間的間隔時間通通計算
如果工作是長期開發 USB 韌體, 弄這台是還有價值的, 不過要準備一台耐用的電腦就是了
他們公司網站已經不見了XD 所以我拿了台 XP 電腦才能用, 機器下面就是電腦

接著開始實驗, 首先是協議特色, USB 是 Polled Bus, 所有傳輸都由 host 發起

上圖上半是 USB 規格書內容, 下半是裝置傳輸資料的封包
裝置先發一個 token 封包, 指定接收的裝置, 告訴裝置要資料
裝置沒有資料要傳就回一個 NAK 封包, 在 atmega16u2 上 NAK 封包是自動回的
用戶韌體不用處理, 只有有資料要回的時候才需要親自處理
從這架構可知 USB 設備是處於完全被動狀態
當裝置要傳資料給 host 時, 只有等 host 來要時才會傳回
不同於 UART 的想傳就傳, 不過看規格書有個例外, 那就是喚醒時

有一些條件要滿足, 這個目前我不知道怎麼做, 這可能要特別設定 host 才行
而一般通訊時就是只能等 host 來要資料, 不過這時間並不長
atmega16u2 的 USB 是 USB2.0 全速 (full speed), 不是 USB2.0 的高速

它就每幾十個 us 就會來問一下有沒有資料, 神煩XD
所以除非是要查 NAK 問題, 不然這封包就濾掉吧

傳輸時有同步信號, 用 SOF (Start of Frame) 來確保同步

可以看到封包分析儀的時間紀錄, 如同規格書寫的全速時每 1ms 發一個
如同 NAK, 這種封包在分析時通常也是濾掉

傳輸資料時除了 control endpoint 外其他都是單方向, 不是 IN 就是 OUT
這裡的 IN, OUT 都是從 host 的觀點去看, host 傳輸給裝置就是 OUT
host 讀取裝置就是 IN, 不管哪個方向傳輸都是由 host 發起

這是 IN 的資料交換流程, 這個圖表放在規格書附錄, 翻了很久才注意到
它是說明有 hub 時的傳輸, 由於我是只想做裝置所以一直跳過這章節
後來才發現傳輸流程是寫在 hub 這章, 再想想, 其實也沒錯
因為如果是電腦, 對外的傳輸一定會經過 hub, 主機板一堆 port 都是裝 hub
一次一起說明比較清楚, 所以如果要看資料交換流程就要看這章
但...這章有一百多頁, 佔整份規格書 1/4, 蠻恐怖的(汗)
所以我只看其中幾張圖, 只注意 hub 後對裝置的傳輸
如上圖, hub 後對裝置的傳輸會有三個封包
先發 IN token, 然後裝置把資料發出, 接著 hub 發 ACK, 這樣就傳送一筆資料
在 atmega16u2 上若是單方向 IN 的 endpoint 只要把資料塞進 FIFO
然後觸發傳輸, 上面這些流程會自動完成


這是 OUT 的資料交換流程, host 先發 OUT 或是 SETUP token
接著會發資料, 完成後裝置回一個 ACK, 這樣就傳送一筆資料
OUT token 是單方向 OUT endpoint 在用的
SETUP token 則是雙向的 control endpoint 在用的
atmega16u2 上遇到 OUT/SETUP token 會自動接收後面的資料
然後保存在 FIFO 中, 程式讀取後清除旗標才會再收下一個 token
若程式沒有清除 FIFO, 接著 host 又發一筆資料過來
atmega16u2 會回 NAK, 然後 host 就會一直塞, 重複塞這筆資料
直到軟體清除 FIFO, atmega16u2 再收一筆回 ACK, host 才會送新資料

接著是連線流程

是有限狀態機, 如果又加上通訊, 例如 USB 無線網卡的韌體
韌體就要同時應付兩邊的狀態, 超機車!
不過如果是全速的 USB 狀態其實不算多, 而且許多狀態一下子就過了
一進入 Configured 狀態通常就只會切到 Suspended
我把 LUFA VirtualSerial 的通訊用封包分析儀截下來
在我的源碼包的 lufa/packets 目錄下有封包截圖, 這是過濾過的
濾掉 SOF 和 NAK 的封包, 有興趣可參考
LUFA 的程式支援多顆 AVR MCU, 同時支援 USB host 和 device
雙模 OTG 的也有, 超猛!但也因此它程式非常複雜
所以我花了些時間研究, 然後只抽出 atmega16u2 需要的, 重寫成比較小的程式
接下來說明將以我程式為主, 經過封包比對功能相等, 不過有抽一點料XD

我程式有三包, 源碼中 usb-priv, usb-cdc, usb-led 這三目錄
usb-priv:建立 USB 連線, 讓 host 識別為 vendor 自訂裝置, 然後什麼也不做
usb-cdc:建立 USB 連線, 讓 host 識別為 CDC ACM 裝置, 然後不斷丟字串
usb-led:usb-cdc 程式繼續寫, 做成可以控制彩色 LED 的設備

首先是 usb-priv, 主要程式都在 usb.c 中
usb_init() 流程都是 LUFA 那裡看來的, 各行功能都有註解, 不過有一點不同
LUFA 沒有清除 endpoint 的行為, LUFA 設定 endpoint 是逐一設定
atmega16u2 有 5 個 endpoint, 每個若要使用都要配置 FIFO
LUFA 的作法是若要配置第 2 個 endpoint, 就把 endpoint 0~4 都配置一次
只不過 0,1,3,4 endpoint 都是讀出值再寫回去, 只有 2 是設新的值
這是為了讓 atmega16u2 的 USB FIFO 記憶體能正確配置, 細節請參考 datasheet
我的裝置固定功能並不打算動態改, 所以改成一次清除所有 endpoint
然後一次把所有 endpoint 完成設定, 然後就不改了
這會發生問題, host 會認定我的 IN 節點有資料而一直來要, 但我卻沒資料給它
於是就 protocol error, 這樣的裝置是可以存取的, 只是分析儀會報錯
後來實驗發現如果我清除 endpoint 以後又執行 reset endpoint 就沒發生了
所以我就這樣寫, 這和 LUFA 的流程有點不同

接著開始連線, 狀態如上面那圖
atmega16u2 在初始化以後清除 DETACH 這 bit 就進入 Attached 狀態
接著應該是 Powered 狀態, 不過那好像和 hub 有關, 我沒有特別處理那狀態
所以就我的程式來說 Attached 和 Powered 是算同一狀態
接著 host 會發 token 來要 descriptor, 此時我的程式進入死迴圈
所有訊息交換都在中斷向量中完成, 這不太好, 但 LUFA 也是這樣寫的
atmega16u2 datasheet 說建議用 poll 的, 不要用中斷
並建議 control endpoint 不要清除 FIFO, 這不恰當
但是 LUFA 把不建議的全都做了, 我不知道該聽誰的XD 最後我是聽 LUFA 的
中斷有兩個, 一個是 USB controller 的, 一個是 endpoint 的
control endpoint 我有設中斷觸發, 當有 SETUP token 時會中斷
於是就進 ISR(USB_COM_vect), 裡面只有一個函數 usb_process_default_pipe()
判斷該做什麼時, LUFA 用了個奇怪的邏輯判斷
LUFA 先判斷 request, 然後才去對 request_type, 我總覺的先判斷方向比較習慣
而且如果從方向去切, 可以看到剛好偶數的 request 都是 GET, 奇數的都是 SET
不過只有 standard request 這樣, 到了 CDC 的就沒有照這規則了
但我還是習慣先選方向, 反正不管是我的還是 LUFA 這兩個都會判斷
誰先誰後只是看心情喜好, 應該都沒差

host 第一個 request 是 GET_DESCRIPTOR, 但是只讀了 8 個 byte 就停
所以我的 usb_ep_send() 要隨時檢查, 停了就要跟著退出
只讀 8 個 byte 我認為應該是為了要 packet size
USB 傳輸每次資料不得超過 descriptor 寫的 packet size
以全速 USB 來說只有 8, 16, 32, 64 這四種選擇
如果要傳輸超過這尺寸的資料就要分多次傳, 例如 64 byte endpoint 傳 65 byte
就要先傳 64, 再傳 1, 如果是傳 64 byte, 還是要分兩次, 先傳 64, 再傳 0
若是傳 63 byte 就只要傳一次, 所以 usb_ep_send() 才會寫的那麼長
另外, 如果是一般 endpoint 上面那樣分割傳完就沒事了
而 control endpoint 就不同了, 我是看分析儀和程式才知道
規格書好像沒看到, 也有可能是塞在 hub 那章我沒看到
control endpoint 如果是 IN 時, 裝置回應後 host 會再發個 OUT 0 byte 出來
如果是 OUT 時, 裝置接收後 ACK 完要再發個 IN 0 byte 給 host, 這樣才算完成
只有 control endpoint 會這樣, 其他 endpoint 不用

host 收了 descriptor 以後發 reset, 此時收到中斷的換成是 ISR(USB_GEN_vect)
atmega16u2 會設 EORSTI, 此時進入 Default 狀態
接著 host 發 SET_ADDRESS, 收到後回 IN 以後再寫進 UDADDR
此時 atmega16u2 就會對這位址的封包回應, 進入 Address 狀態
接著 host 會再讀一次 descriptor, 這次是把所有 descriptor 都讀取一次
讀完以後會設 configuration, 我沒有去研究 host 是怎麼選的
但我的裝置只有一個 configuration, host 也沒得選
所以下一步就是 SET_CONFIGURATION, 完成回應後裝置就進入 Configured 狀態
由於 usb-priv 是設定成 Vendor Specific 設備, host 除非有驅動不然不知道如何操作
所以到這裡就停止通訊, 什麼事也不做, 就只是系統裡可以看到這設備
這個設備的通訊封包放在該目錄下的 packets.png
ubuntu 電腦 dmesg 可以看到:

usb 1-7.1: new full-speed USB device number 78 using xhci_hcd
usb 1-7.1: New USB device found, idVendor=03eb, idProduct=2044
usb 1-7.1: New USB device strings: Mfr=1, Product=2, SerialNumber=220
usb 1-7.1: Product: WT-13
usb 1-7.1: Manufacturer: WuKC
usb 1-7.1: SerialNumber: 08886449

不過換成 lsusb 則變成:

Bus 001 Device 068: ID 03eb:2044 Atmel Corp. LUFA CDC Demo Application

裝置名稱和廠商字串不一樣, 可能字串來源不同
另外, host 發的 address 和裝置得到的 address 是不一樣的, 這應該和 hub 有關
從 USB 架構可以感覺到 hub 是非常難做的設備, 它要能裝成一個正確的設備
同時還要有完整的 host 功能, 代替上層 host 向下層裝置要資料, 這真難搞!

接著我們繼續增加程式, 接著看 usb-cdc 這程式, 把 CDC ACM 功能加上去
有興趣可以用 diff 比較兩目錄的 usb.c, 它有些不同
增加了 CDC 需要的 descriptor, 這些設定要參考 USB CDC 1.1 規格書
這規格書要 Google 找, usb.org 上放的是 CDC 1.2 和 1.1 的差異, 不是完整版
我翻了老半天翻不到我要的資料, 後來才發現那只是更新檔, 被陰了
CDC 加上後增加 3 個 endpoint
1 個 interrupt IN, 1 個 bulk OUT 和 1 個 bulk IN endpoint
一定要這三個, 少了會出錯, 其中 interrupt IN 沒有功能
我看 LUFA 要呼叫 CDC_Device_SendControlLineStateChange 才會去發資料
所以這我就不寫了, 只是配置起來讓 host 看到, 然後就放著不管

LUFA 的 CDC_Device_CreateStream() 我也有做, 沒幾行
我的寫在 usb-cdc.c 的 usbcdc_create_stream()
不過 put 函數不同, 前面提到發資料若大於 packet size 要分割
我怕麻煩所以只送 packet size 以內的封包XD
快超過了就發, 等發完再塞 FIFO, 這樣比較簡單, 一樣能用
這段程式寫在 usbcdc_put() 中
usb-cdc 做的事情和我改的 LUFA 範例一樣每隔約一秒印一行 A=60001 字串
我們可以比較編譯結果:

LUFA VirtualSerial:

AVR Memory Usage
----------------
Device: atmega16u2

Program:    5046 bytes (30.8% Full)
(.text + .data + .bootloader)

Data:        114 bytes (22.3% Full)
(.data + .bss + .noinit)



我的 usb-cdc:

AVR Memory Usage
----------------
Device: atmega16u2

Program:    3994 bytes (24.4% Full)
(.text + .data + .bootloader)

Data:        159 bytes (31.1% Full)
(.data + .bss + .noinit)

記憶體多用了 45 byte, 程式空間少了 1070 byte, 還行, 不過就只能用在 atmega16u2
如果有開發多顆 AVR 的需求, LUFA 是值得考慮的選擇, 只要依照軟體協議走
就可以獲得愉悅的開發體驗, 程式空間雖然多一點但考慮到跨 MCU 就還能接受
不過使用難度並沒有比我的少, 得找出 descriptor 定義在哪, 還要知道怎麼設
我的只要看規格書全部塞進陣列即可, 這種只有用一次的數字我不想定 macro
寫在註解就行了, 知道這代表啥即可

最後, 我做一個簡單的應用, 還是彩色 LED! 只要一隻腳太簡單, 就拿來當範例
程式放在 usb-led 目錄, 然後把前篇 cc2540 的 UART 協議搬過來
我們就可以在 ubuntu 上用 minicom 直接通訊, 由於是假的 usb-serial
minicom 不管設啥都會動XD
設定 baudrate 會走 CDC 的 SET_LINE_CODING request
我的程式收到了就把參數存起來, 然後什麼也不做就回 ACK
打一開始就沒有 UART 了當然什麼也不用做XD

minicom 設定 ttyACM 到 1Mbps 一樣會有回應XD

接著實施壓力測試, 我的源碼包 host-src 目錄下有個 stress.c
在 ubuntu 上編譯後執行, 用它去開 ttyACM
./stress /dev/ttyACM0

不斷的塞資料, 然後等回應, 收到回應後計算時間
概略的計算資料量然後除時間, 可以算出約 70 KB/s 的速度, 蠻驚人的
如果換成 UART 要換晶振且只能上到 115200, 這樣才 14 KB/s 而已
用 USB 直接塞整整快了五倍

接著我把以前 CNC 界面軟體的程式抽出來試驗
套到 android 在 eclipse 裡的範例 MissileLauncher 中
丟到 A20 的 CubieTruck, 系統是 Android 4.2

APP 開啟後詢問權限, 此時燈是預設顏色


同意權限按 Fire, 燈全白, 能動!

然後把這 APP 裝到前篇升級到 android 7.1 的手機上

我這手機 USB 不會供電, 電源要另外給, 故接上行動電源, 一樣會詢問權限


燈全白, 一樣會動

我們用這顆 AVR 做了虛擬的 USB serial, 可以從電腦或手機收發資料
只要一顆 MCU 就可以做簡單控制, 而且還是 QFN 封裝, 體積超小
AVR 用的 USB 以前有另一個選擇:VUSB
給沒 USB 硬體的 AVR 用的軟體 USB, 用精密的組合語言控制 GPIO 弄出的 USB
十分變態XD 這韌體有用在 USBASP 上, USBASP 用的 AVR 是 atmega8a, 沒有硬體 USB
現在我們有了硬體 USB 又超低價的 MCU, 我們沒有必要再去用那種變態的東西XD
VUSB 只更新到 2012 年底就停了, 可能後來這些 8u2, 16u2 系列的 MCU 出現就改變了
不過 16u2 裡的 USB 似乎不是該公司的新產品
以前 Atmel 公司網站給的參考資料是 2008 年的另一顆 MCU 的 application note
這顆的 USB IP 可能是從另一顆移過來的, 還是說這顆 16u2 其實早就存在很久
這我就不清楚了, 只知道開始很多人玩這顆是近幾年的事, 從裝到 Arduino 上開始

最近找資料才發現 Atmel 賣給 Microchip 了, 想不到這 8051 老招牌公司也有賣掉的一天
Microchip 的產品我在唸書時也用過, 這 blog 還有幾篇文章
他們家 MCU 只有 Windows 工具, 所以我換平台以後就再也沒碰了, PICkit 就扔在抽屜裡
希望 AVR 開放的精神不會因換老闆改變, 不然我可能又要換方案了

2 則留言:

  1. 3年前我有預測Microchip應該去做ARM,買是最快的。在去年就動手買下atmel。然後PIC32是第一個死的產品線。
    Atmel的ARM是續存產品應是沒有問題,AVR就麻煩了。MCHP已是八位元MCU最大廠商,PIC,8051,AVR三條產品線,真的要合,AVR可能是第一個目標。但應該也是好幾年後的事了。
    手機廠也差不多,本來看好郭董的Infocus還買了一支,自從有了Sharp,Infocus就沒有新機了。
    這就是現實!

    回覆刪除
    回覆
    1. https://www.sec.gov/Archives/edgar/data/932111/000095010315003773/dp56103_425.htm
      這應該是併購另一家公司時的投影片
      第 9 頁他們家自己的調查, atmel 市佔雖不是第一但也有第四
      若要砍也是砍 PIC32 吧, 我不認為他們會做弄掉 AVR 這種事
      但削減資源就有可能, 就怕他們讓 avr 退出 gcc, 這樣下一步就可能是砍掉
      我比較希望看到把 PIC 弄的和 AVR 一樣有 gcc 主流支援, 這樣就有多的選擇
      再看看吧, 如果砍了就去找新唐或是 STM
      >自從有了Sharp,Infocus就沒有新機了。
      那是當然的, 另一張牌貼上去簡直刺眼, 閃閃發光超好用XD
      搞不好是同一批人馬只是換了外皮也說不一定XD

      刪除