# Arduino PWM # 概述 Arduino 是一塊很方便取得、成本低廉(約台幣 400 元,NI myRIO 10 分之一有找)的單片機。除此之外,其硬體實作為 CC 授權,其官方開發程式 Arduino IDE 亦為開放原始碼專案。因此,使用該硬體作為研究微處理器與周邊硬體的互動,不失為一種方便的選擇。另外,以 Arduino UNO 為例,該處理器使用 Atmel 328/P 處理器,及較精簡的 AVR 硬體架構,不若 ARM 或 x86-64 之處理器複雜,理解上較容易。 本篇主要簡單描述 Atmel 328/P 之架構與指令集,並且試著用 Arduino 設計與本次使用 NI myRIO 伺服馬達 PWM 相同之實驗。 # 硬體資訊 ## 硬體架構 ![](https://i.imgur.com/2FzkALg.png) Arduino 上用的 Atmega 328 並不是常見的 ARM 或 x86-64 架構,而是使用 Atmel AVR 架構。根據官方文件,可注意到該架構有以下特點: 1. 架構採用 RICS 指令集,即每道指令的位元長度均相同。「Most AVR instructions have a single 16-bit word format.」 2. 有 32 個通用的 8 bit 暫存器(register)。而其中有 6 個暫存器可以視為一個 16-bit 暫存器使用(文件中以 X, Y, Z 命名),其主要功能為存放資料在記憶體中的位址,增加處理效率。 3. 處理器採用哈佛架構,即「程式記憶體」與「資料記憶體」是分開的(不像現在多數的筆電或桌上型電腦,程式的資料與程式的指令共用記憶體)。 4. 有 status ragister。當 ALU 做運算時,若發生某些特定事件(如相減為 0、發生 overflow 等等)時,會將該暫存器儲存特定的值。透過檢視該暫存器的值,可以知道運算狀況(如兩值 NAND 之結果若為 0,則可之 2 值相等,並將相應的 flag 設立,藉此可實作各程式語言中 `if` `while` 等指令的功能)。根據官方文件說明「The Status register is updated after all ALU operations」,即狀態暫存器會在每一次指令執行之後被設定。 5. 二階 pipelining ## 指令集 1. 運算類:加減法、邏輯運算。有做暫存器之間資料運算的指令,以及將暫存器與一個常數做運算的指令。 2. Branch 類:有 direct jump 與 conditional jump。 3. LOAD/STORE 因為發現這些指令跟 PWM 實驗沒有直接關係,所以先省略。 # ATmega328/P 硬體時脈 在 ATmega328/P 中所使用的時鐘(clock)有以下幾種: ![](https://i.imgur.com/iilqaH9.png) 在這邊需要特別注意的是 clkIO,該時脈會被 PWM 的功能(或說 Wave Generation 硬體)作為參考基準。而可注意到該時脈係由 $clk_{SYS}$ 預先經過一個 prescale 之後作為輸入,再經由 AVR Clock Control Unit 處理得到 $clk_{IO}$。因此,該 System Clock Prescale 可以視為是一個全域性的 prescale。而該 prescaler 係透過設定 CLKPR 暫存器之設定來達成,該暫存器相關位元如下: ![](https://i.imgur.com/EO9eMfZ.png) 1. CLKPCE:意義為「Clock Prescaler Change Enable」。該位元設成 1 時,才能對該暫存器其他位元進行寫入。另外,該位元亦有特殊之設定規則:當該暫存器所有位元都正在被設成 0 時,才能寫入這個位元(即當下寫入 0x80 時,才能將該位元設定成 1)。該位元設定為 1 之後,有 4 個 cycle 的時間可以將後面 CLKPS 的 4 個位元寫入非 0 位元,該位元即代表 prescaler 縮放的程度。當寫入一發生,或是沒有寫入但超過 4 個 cycle 時,CLKPCE 將會被重設為 0。 2. CLKPS:決定 prescale 縮放的程度,相關規範如下表: ![](https://i.imgur.com/NlwfY5q.png) 而超過 1000 的數值沒有定義。可以發現規則為: $$Clock\ Division\ Factor = 1 << CLKPS$$ 若使用 Arduino IDE,則可使用如下的程式: ```C++= noInterrupts(); CLKPR = _BV(CLKPCE); //啟動寫入模式 CLKPR = _BV(CLKPS0); //設定位元數目 interrupts(); ``` 這裡使用的 _BV() 為 Bit Value 之意,為 Arduino 中為方便程式之巨集,本質上是算術 shift,並且做適當宣告,避免編譯器因為優化省略對該變數的寫入。另外,實際上調整處理器暫存器之位元時,有許多巨集可使用,如這邊使用 _BV(CLKPS0) 將形成僅一個 1 之一組位元,該 1 的位置在 CKLPS 之第 0 位元。而最後將這些位元遮罩進行 OR 運算,便可得到設定好的暫存器位元。 如前述設定方法,該暫存器必須確保輸入為 0x80 時,方能將該暫存器進入寫入模式,而 `noInterrupts()` 函數與 `interrupts()` 區間將確保設定暫存器時,不會受到硬體中斷影響。然後我猜 CLKPR 等等暫存器都有 vilatile 宣告,即編譯器優化時,無論如何寫入該變數的指令規定不可因優化省略。以下為實際設定 CLKPR 暫存器中的 CLKPS 位元時,PWM 時脈的變化: ```=C++ void setup() { noInterrupts(); CLKPR = _BV(CLKPCE); // 啟用設定模式 CLKPR = _BV(CLKPS0); // 設定 CLKPS 第 0 位為1 interrupts(); } void loop(){analogWrite(5, 150);} ``` 1. CLKPR = _BV(CLKPS0): 即 CLKPS[3:0] = 0001, 縮放倍率 2, PWM 頻率 488 Hz ![](https://i.imgur.com/c00PloK.jpg) 2. CLKPR = _BV(CLKPS1): 即 CLKPS[3:0] = 0010, 縮放倍率 4, PWM 頻率 244 Hz ![](https://i.imgur.com/KYkXzoo.jpg) 3. CLKPR = _BV(CLKPS0) | _BV(CLKPS2): 即 CLKPS[3:0] = 0101, 縮放倍率 32, PWM 頻率 30.5 ![](https://i.imgur.com/JURhyfV.jpg) 4. CLKPR = _BV(CLKPS0) | _BV(CLKPS1) | _BV(CLKPS2) 即 CLKPS[3:0] = 0111, 縮放倍率 128, PWM 頻率 7.63 ![](https://i.imgur.com/cGOLoaX.jpg) 5. CLKPR = _BV(CLKPS3) 即 CLKPS[3:0] = 1000, 縮放倍率 256, PWM 頻率 3.81 Hz ![](https://i.imgur.com/tpsCAGA.jpg) 雖然 ATMega 328/P 本身硬體的時脈為 16MHz,但實際上系統在輸出 PWM 時,會另外對各自的 timer/clock 進行 prescale,使得輸出並不為 16MHz。詳細設定將在後面描述。 # PWM 的硬體實作 ## 演算法 假定有個 8-bit 暫存器(數值範圍 0 ~ 255)負責計算時間,每經過某個固定數目的時脈時,硬體會將該暫存器數值 + 1。考慮以下的邏輯電路: ``` voltage = counter > COMPARE ? LOW : HIGH; ``` 舉例而言,若令 COMPARE 大小為 128,則會在 [0, 128] 的區間中輸出低電壓; 會在 [129, 255] 之間輸出高電壓。而當 counter 數值超過 255 時,即發生溢位,因此數值又歸 0 ,重新回到 [0, 128] 的區間。如此重複,便可產生 duty cycle 約為 50% 的 PWM 訊號。 更一般地說,假定有一暫存器之範圍為 [BOTTOM, TOP],透過上述的演算法,我們能夠在 [BOTTOM,COMPARE] 的區間內輸出高電位,[COMPARE + 1, TOP] 的區間輸出低電位,進而製造工作週期解析度為 8 位元的 PWM 訊號。 在 Arduino 中,COUNTER 對應到 TCNTn 暫存器。COMPARE 對應 OCRnx 暫存器。這裡 n 可能代表 A 或 B ,而 x 可能是 0、1、2。舉例來說,官方文件中提到 19.2.2 節提到「... The double buffered Output Compare Registers (OCR0A and OCR0B) are compared with the timer/ counter value at all times. The result of the compare can be used by the waveform generator to generate a PWM or variable frequency output on the Output Compare pins (OC0A and OC0B)....」,其描述的部分即為上面介紹的演算法,而 OCR0A、OCR0B 等暫存器在文件(以及接下來敘述中)以 OCRnx 泛稱。 官方文件中,實行胎演算法的 block diagram 大致如下: ![](https://i.imgur.com/3iL4VX0.png) 該硬體在 Atmel 328/P 文件中稱作「Output Compare Unit」。在文件 19.5 節亦提到:TCNT0 暫存器會不斷與 OCR0A 與 OCR0B 暫存器比較。若 TCNT0 暫存器中的數值與 OCR0A 或 OCR0B 中設定之數值相等時,便會將 Output Compare Flag 做設定。該 flag 在硬體中對應的暫存器分別是 OC0A 或 OC0B。 然而,上述討論僅涵蓋了工作週期的調整,並沒有提到該怎麼改變 PWM 的頻率。PWM 之頻率,取決於「經過多久時,會將暫存器中之數值 + 1」。故可知道,邏輯電路中內建 clock 的時脈對 PWM 訊號的輸出會有所影響。因此,藉由前面提到的 prescaler 及 CLKPR 暫存器相關參數調整,可以調整 PWM 的倍率。然而,prescaler 僅能對時脈進行特定倍率的縮放。若需要進行更詳細的調整,需要更進一步的硬體知識。 ## Atmega 328/P 中的計時器 由上述討論中,似乎 COMPARE 值僅能調整 PWM 的工作週期,而 PWM 頻率高低則必須由硬體 clock 做調整。然而,實際上 ATmega 328/P 的計時器有各式各樣的模式,可以透過不同暫存器模式之設定,做出非常多元的變化。為此,必須對 Atmel 328/P 中之計時機制有初步了解。 在 Atmel 328/P 處理器中,有 3 個計時器(Timer/Counter, TC),分別為 TC0、TC1、TC2。其中 TC0 為 8 位元之計時器、TC1 為 16 位元之計時器 ,而 TC2 為 8 位元之計時器。 TC0 之硬體架構如下: ![](https://i.imgur.com/eZhAtoy.png) TC1 之硬體架構如下圖: ![](https://i.imgur.com/aaIwc3w.png) TC2 之硬體架構如下圖: ![](https://i.imgur.com/iJOPKgw.png) 需注意的是:即使 TC2 同為 8 位元計時器,其構造仍與 TC0 有些許不同,而官方文件中亦將 3 者分開敘述(表示文件會看到升天)。實際上,在輸入的部分,TC0 與 TC2 就有明顯不同。TC0 如下: ![](https://i.imgur.com/97nbelo.png) TC2 如下: ![](https://i.imgur.com/Y7nQ4oN.png) 由於將 3 者並陳比較差異,會讓整份報告看起來非常冗贅。因此這裡僅說明 TC0 之計時機制、設定、ABI 以及不同的計時模式。另外一個理由是 16 位元的暫存器,實際上是由 2 個 8 位元暫存器合併,而對於該暫存器的寫入牽涉到最小操作(atomic operation),即如何保證兩個 8 位元暫存器寫入過程沒有中段,且編譯器不會把寫入優化成奇怪的順序,再做下去專題就要 GG 了,所以先暫時跳過。 ## TC0 ![](https://i.imgur.com/eZhAtoy.png) 在 ATmega 328/P 中,TC0 的計時儲存在 TCNT0 暫存器中(圖示為 TCNTn),而該暫存器會與 2 個暫存器進行比較,分別是 OCR0A 與 OCR0B。該部分的電路在官方文件稱作「Ouput Compare Unit」,獨立進行上述「演算法」部分之比較工作。 實際上,若考慮可以將 TCNTn 達到 255 之前便進行歸零,或是將計數的最大值 TOP 進行變動,則可以進行更多元的計時任務,而 ATmega 328/P 亦是靠各種功能豐富的模式進行 PWM 的設定。這些不同模式都依靠一組 2 個暫存器 -- TCCR0A 與 TCCR0B 進行設定。 需注意的是:TCCR0A 與 TCCR0B 兩者角色並「非」 TCCR0A 負責設定 OCR0A 、 TCCR0B 負責設定 OCR0B ,而是兩者有不同功能。相關模式將會在後面進行介紹。 1. 選擇計時用的訊號來源:該單元依照模式不同,來決定計數間該使用哪邊的訊號。該部分可以直接使用 clock 訊號作為計數標準(即多工器選擇 Edge Detector); 或是將訊號通過 pre-scale,再進行計數(即多工器選擇 From Prescaler) ![](https://i.imgur.com/S3GIfVX.png) 1. 對 clock 訊號進行 rising edge-detect,即以「每次時脈之電位由低轉高時」作為計時之標準。 ![](https://i.imgur.com/xQhRzOf.png) 2. 先將 clock 訊號通過 prescaler ,再使用 prescaler 輸出。 ![](https://i.imgur.com/CccjXM9.png) 需注意的是:這裡的 prescaler 不同於前述 CLKPR 暫存器設定之 prescaler,而是 TC0 中另外的 prescaler (所以,若這個 prescaler 也啟用,時脈計算實際上會經過兩個 prescaler)。而上圖中之 clkIO 訊號實際上是已經過 CLKPR 暫存器對應之 prescaler 設定過之時脈。而 TC0 本身之 prescaler 則是透過另外 2 組暫存器:TCCR0A 與 TCCR0B 進行設定,而這兩個暫存器也提供 TC0 做其他設定。 由圖中可以發現:在沒有 prescaler 的狀況下,該硬體輸出永遠是 1; 而有 prescaler 的狀況下,特定倍數個 clock 時脈之後,才會在記數器輸出一高電位啟動 Control Logic。 2. 檢測特定計時的事件:如偵測上述演算法中提到的「計時值是否超過 TOP 值」等等。 ![](https://i.imgur.com/7I6PTNb.png) 主要事件有下面 4 樣: 1. MAX:該暫存器是否超過 8-bit 暫存器最大值 0xFF (若為 TC1 則為 16-bit 之最大值 0xFFFF)。若達到則輸出 MAX 訊號。 3. BOTTOM:該暫存器是否達 0,若是則輸出 BOTTOM 訊號。 4. TOP:檢測是 TCNTn 中之數值是否「超過某最大值」。由上述硬體架構圖可發現,不論是 8 位元計時器,或是 16 位元計時器,該最大值可為某個固定的最大值(Fixed TOP Value),而該值在 TC0 為 0xFF; 或是由 OCRnA 做動態調整(可注意 Fixed TOP Value 與 OCRnA 連接的 multiplexer)。若為 TC1 則可挑選為 0xFFFF 或 ICRn 暫存器中之數值。 6. OCnA/OCnB:當 TC 之值超過設定之 OCRnA/OCRnB 之值時,將該設定該 flag 。該 flag 可以作為觸發硬體中斷機制的方法。亦能夠在規格書 16.1 中的內容發現 Compare Match A 等等事件: ![](https://i.imgur.com/apvE9HP.png) 而該如何設定這幾個計時器?可以參考 TCCRnA、TCCRnB 兩個暫存器的數值決定: ![](https://i.imgur.com/WFT2EmJ.png) ![](https://i.imgur.com/ZpgRtlm.png) 如前章節之描述,TCCR0A 與 TCCR0B 兩者之 ABI 並不對稱。其主要設定之位元如下: 1. WGM0[1:0] 與 WGM2:意思為 Wave Generation Mode 的簡稱。可以視為 3 位元的一個變數 WGM0[2:0],其中 WGM[1] 與 WGM[0] 位於 TCCR0A,WGM0[2] 位於 TCCR0B。根據其位元設定方式,可以決定如下不同的模式: ![](https://i.imgur.com/z26Dzuk.png) 而各種模式會在後面詳述。 2. COMA 與 COMB:設定兩個 Ouput Compare Unit 的輸出方式。COMA 對應與 OCR0A 比較後的輸出; COMB 設定與 OCR0B 比對的輸出。設定為 0x2 時,比對成功前輸出低電位,比對成功後輸出高電位; 設定為 0x3 時則相反。如下圖: ![](https://i.imgur.com/vgwpwMp.png) 而設定成 0x1 時,其功能為「toggle」,即將前後高低電位做反轉,而不論比對成功前為低電位或高電位。 3. CS0:設定 TC0 自身的 prescaler。不同位元代表不同的大小,附表如下: ![](https://i.imgur.com/m7mpV6x.png) ![](https://i.imgur.com/DgBx1PQ.png) 4. FOC0x(FOC0A 與 FOC0B):當這 2 個位元被設定時,會強制視為 x 比對成功,不論實際上的 TCNTn 值為何。 ## 模式 ### Normal Mode: WGM0[2:0] = 0x0。 放任他 overflow 作為工作週期。不會進行歸 0(因為一 overflow 就會自動歸 0),並且在發生 Overflow 時,設定 TOV0 flag。 ### Clear Timer on Compare Match WGM0[2:0] = 0x2。 就是多工器走虛線,以 OCR0A 作為比較標準,並且在每次 TCNTn == OCR0nx 時,將 TCNT 歸零。邏輯電路輸出如下: ![](https://i.imgur.com/zsvJ0rU.png) 可發現 TCNTn 之輸出一路遞增,直到 OCnx interrupt flag 被設定後(即 當 TCNTn == OCRnx 發生時),觸發改變。需注意的是,OCn 電位可以由 COMnx[1:0] 之設定,進行正反變化。 ### Fast PWM Mode WGM[2:0] = 0x3 或 WGM[2:0] = 0x7。當設定成 0x3 時, 其 TOP 值會是 0xFF(即多工器走 Fixed TOP Value); 若設定成 0x7,則多工器同樣走虛線,以 OCR0A 作為比較標準。這時候 Channel A 並不能產生 PWM 工作週期的改變(因為本來用於設定 PWM 工作週期的 OCR0A 暫存器值,必定與計數之最大值相同),但可以由 OCR0A 之值使 Channel B 輸出的 PWM 頻率產生變化(由 0xFF 重新計數一次,變成由每 OCR0A 重新計數一次),並且透過 OCR0B 之值,使 Channel B 之 PWM 工作週期產生改變。因此可以說是一種權衡。 ![](https://i.imgur.com/o1Z6RiN.png) 而如前介紹 TCCR0x 暫存器的部分所提到,這時候可以使用 COM0A、COM0B 等位元進行高低電位輸出的互換。 1. COMnx[1:0] = 0x2 時,預設為高電位; 在 TCNTn == OCRnx 開始,到 TCNTn 達到 MAX 之前,將電位變成低電位。 2. COMnx[1:0] = 0x3 時,與上述相反:預設為低電位; 在 TCNTn == OCRnx 開始,到 TCNTn 達到 MAX 之前,將電位變成高電位。 由 8-bit 的 TC 之方塊圖可發現 TC0 與 TC2 均有「若使用 OCRnA 進行 PWM 頻率調整,則 A Channel 就無法調整工作週期」的問題。然而,若是使用 16 位元的 TC1,則因為重新計數的最大值可由獨立的 ICRn 暫存器設定,而非 OCR2A 或 OCR2B,因此可以避免該問題發生: ![](https://i.imgur.com/aaIwc3w.png) ### Phase Correct PWM Mode WGM0[3:1] = 0x1 或 0x5。當設定為 0x1 時,最大值為 0xFF; 當設定為 0x5 時,最大值為 OCR0A 暫存器中的值。類似上述 Fase PWM,只是此時 TCNTn 之行為為一路遞增至 0 ,接著一路遞減,而不如 Fast PWm 模式中,當 TCNTn 暫存器遞增至最大值後,就職接將該暫存器歸 0: ![](https://i.imgur.com/GjcdBX6.png) 該模式稱作 Phase Correct 之理由為:若 OCR0A 為定值,則產生的 PWM 方波的中央位置,恰好位於 TCNT0 的波鋒或波谷(視 COM0 中的位元決定輸出模式)。不像前述 Fase PWM 之鋸齒波鋒與方波中央有一相位差。 與前述 Fast PWM 模式相似地:若將 TCNT0 最大值設為 OCR0A 暫存器中的值(即 WGM0[2:0] = 0x5),意味著將會犧牲掉 Channel A 的工作週期調整能力,以換取 Channel B 調整 PWM 頻率的能力。 另外,除了 Channel A 將失去調整 PWM 工作週期之能力,因為計數之最大值受到影響,因此工作週期的解析度也會隨之下降(一個週期中由 0 ~ 0xFF 變成 0 ~ OCR0A)。但若需要高解析度,則可考慮使用 16 位元之 TC1。 # 直接進行 PWM 頻率設定 這裡使用 TC0 的 Phase Correct PWM Mode,並且設定為 0x5,即 TCNT0 最大值透過 OCR0A 暫存器中之值進行調整,藉此調整細部的頻率。由前述說明可知: 1. 由於 Phace Correct Mode 的計數方式得知,每 2 倍的 OCR0A 值便會重新計數一次,所以 PWM 的頻率為: $$f = \frac {16000,000}{2 \cdot P_G \cdot P_{CH}\cdot OCR0A}$$ 其中 $P_G$ 為 CLKPS 中之 prescaler 之縮放倍率; $P_{CH}$ 為 TC0 中 prescaler 之縮放倍率; OCR0A 為 OCR0A 暫存器中之值。因此,若已知希望的 PWM 頻率 $f$ ,則可知道所有調整參數之間,需要滿足: $$ OCR0A = \frac {8000,000}{f \cdot P_G \cdot P_{CH}}$$ 其中 PG 為 1, 2, 4, 8, 16, 32, 64, 128, 256; PCH 為 8, 64, 256, 1024 1. 選取頻率 f 2. 計算 8000,000 / f 3. 估計並選取適當的 n, 使得: $$\frac {8000,000}{f }\cdot \frac {1}{2^{n}} < 255$$ 並且透過 $P_G$, $P_H$ 的值湊出 2 的 n 方。 4. 將 OCR0A 之值設定為: $$ OCR0A = \frac {8000,000}{f \cdot P_G \cdot P_{CH}}$$ 即得解析度為 OCR0A 之 PWM 訊號。 (突然發現這個貌似可以當作 Lab Manual 的實驗步驟 XDD) 舉例來說,以實驗用的 GWS S03N 伺服馬達為例,需要 PWM 頻率在 50Hz,工作週期大約是 2.5% ~ 7.5%,則調整 PWM 之步驟如下: ![](https://i.imgur.com/lYX4N20.png) 1. $f = 50Hz$ 2. 可知: $$\frac {8000000}{50} \frac {1}{2^{10}} = 156.25 < 255$$ 因此 n = 10,這裡將 $P_G$ 設為 1, $P_{CH}$ 設為 1024。 3. 使用下列程式進行調整: ```C++= void setup() { /* 將系統預縮放倍率調整成 1*/ noInterrupts(); CLKPR = _BV(CLKPCE); CLKPR = 0; interrupts(); /* 重設所有設定 */ TCCR0A = 0; TCCR0B = 0; TCNT0 = 0; /* 調整成模式 0x5,並將 TC 預縮放倍率設成 1024*/ TCCR0A = _BV(COM0B1) | _BV(WGM00); TCCR0B = _BV(WGM02) | _BV(CS02) | _BV(CS00); OCR0A = 156; } void loop(){analogWrite(5, 20);} ``` 得到如下結果: ![](https://i.imgur.com/UaXVNQw.jpg) 可發現理論預測工作週期為 $20 / 156 \approx 0.12$,與示波器量出之結果相近。另外,若調整 analogWrite() 函數中的值,可以得到如下結果: 1. analogWrite(5, 50) ![](https://i.imgur.com/JMG0SW2.jpg) 2. analogWrite(5, 100) ![](https://i.imgur.com/P6Dyg6p.jpg) 3. analogWrite(5, 120) ![](https://i.imgur.com/bsUGiUA.jpg) 4. analogWrite(5, 150) ![](https://i.imgur.com/elJBAlf.jpg) 5. analogWrite(5, 156) ![](https://i.imgur.com/CUoKh72.jpg) 而由解析度可計算: $$\frac {1}{156} \approx 0.641\%$$ 因此,可接受 PWM 能轉動的範圍約在 4 ~ 12 之間(也就是非常糟糕的意思)。這恰好跟實驗碰到時使用 myRIO 的狀況非常像:伺服馬達沒辦法做到精確地選轉,而是一次旋轉很大的角度。 我使用型號類似的 S03T 2BB 進行測試,analogWrite(5, n),n 在 11 至 12 中間時大約為靜止,8, 9 為順時針旋轉; 13, 14 為逆時針旋轉。因為 8 位元解析度太差的關係,所以沒辦法做很精細的調整。但至少有基本的作法。另外, # 相關函式庫 ## Servo.h ```C++ #include<Servo.h> #define SERVO_PIN 9 #define SERVO_POS 180 Servo test_servo; void setup() { test_servo.attach(SERVO_PIN); Serial.begin(9600); } void loop() { test_servo.write(SERVO_POS);} ``` 1. SERVO_POS = 180: ![](https://i.imgur.com/azCT9vF.jpg) 2. SERVO_POS = 135: ![](https://i.imgur.com/7j5TOyU.jpg) 3. SERVO_POS = 90: ![](https://i.imgur.com/rU2KSLP.jpg) 4. SERVO_POS = 45: ![](https://i.imgur.com/d9BaRj8.jpg) 5. SERVO_POS = 0 : ![](https://i.imgur.com/dPYDyEi.jpg) 可以從平均電壓觀察到平均電壓確實改變,符合 PWM 「以數位電壓開關模擬平均電壓」的精神。另外,Servo 函式庫使用的是 16 位元的 TC1 計時器,因此不像剛剛使用 8 位元 TC0 時,僅能產生解析度不到 255 的 PWM 訊號。 ## analogWrite() ```C++= #define PWM_PIN 9 void setup() {} void loop() {analogWrite(PWM_PIN, 0);} ``` analogWrite(PWM_PIN, 0) ![](https://i.imgur.com/kA0TmPs.jpg) analogWrite(PWM_PIN, 64) ![](https://i.imgur.com/Vx4PtA0.jpg) analogWrite(PWM_PIN, 128) ![](https://i.imgur.com/2k8g74T.jpg) analogWrite(PWM_PIN, 192) ![](https://i.imgur.com/GLjfBoh.jpg) analogWrite(PWM_PIN, 255) ![](https://i.imgur.com/JxGLUT7.jpg) ## delayMicroseconds() 實際上 PWM 可以用以下程式進行模擬: ```Arduino #include<Servo.h> #define PWM_PIN A0 #define PERIOD 20000 #define DUTY_TIME 1600 void setup() { pinMode(PWM_PIN, OUTPUT); } void loop() { digitalWrite(PWM_PIN, HIGH); delayMicroseconds(DUTY_TIME); digitalWrite(PWM_PIN, LOW); delayMicroseconds(PERIOD - DUTY_TIME); } ``` 若將輸出連接示波器,可得大如以下的波形: ![Uploading file..._wfdp717yg]() 然而該 PWM 訊號品質並不是很好,因此我不敢把他接到伺服馬達上。 # 問題討論 1. # References 規格書: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega328_P%20AVR%20MCU%20with%20picoPower%20Technology%20Data%20Sheet%2040001984A.pdf http://coopermaa2nd.blogspot.tw/2011/07/from-arduino-to-avr.html