# 單晶片lab2結報 ###### tags: `arduino` ###### 實驗日期 : 2021/09/30 ## 上課教材 - [UART、密碼鎖、計步器](https://hackmd.io/@G8HrHAUqQyCt9mHFYW05UA/rJSzxDQ-K#Arduino%E8%AC%9B%E7%BE%A9%EF%BC%9A%E8%A8%88%E6%AD%A5%E5%99%A8) ## lab1 ### SPEC **密碼鎖** - 自訂一個密碼(ex:`1234`),使用keypad來輸入值,輸入完成後按`A`送出。若輸入值與密碼相同則印出`Unlock!`,錯誤則印出`Wrong passwords!`。 ### 實現方法 ```cpp if (enter != NO_KEY) { if (enter == 'A') { break; } else { value[count++] = enter; Serial.print(enter); } } ``` 將輸入的字串存於字符陣列,但`'A'`不儲存。 ```cpp if (strcmp(value, password) == 0) { Serial.println("Unlock!"); } ``` 使用[C-string字串處理的函式](https://hackmd.io/@HsuChiChen/c-style-string),與正確密碼的字符陣列去做比較。 ## lab2-1 ### SPEC 承上題,增加2項功能 : 1. **修改密碼**。 2. **添加驗證碼**(透過七段顯示器顯示經過`random()`得出的3位驗證碼,並在輸入錯誤時再隨機變換一組驗證碼直到輸入正確)。 ### 實現方法 延續lab1程式 ```cpp if (enter != NO_KEY) { if (enter == 'B') {/*revise password*/} else if (enter == 'A') {/*enter password*/} ``` 分2個個模式 - 按`B`為修改密碼,按`A`為輸入密碼。 ```cpp void input(char terminal_char, char value[]); void input_3(char terminal_char, char arr[]); ``` 2個subfunction中**相同點為傳入之參數** : - 第1個參數判定keypad輸入終止字串的字符。 - 第2個參數傳入儲存之字串陣列。 **不同點1為使用時機與功能** : - 前者是輸入並儲存4-digit的數值。 - 後者是輸入並儲存3-digit的數值(驗證碼),並且要在等待使用者輸入的同時,讓**七段顯示亮燈**,關於亮燈我是寫進一個`class`,要用到的時候就可直接呼叫成員函數`show_seg.show(verification);`,詳見[lab4實現方法第2點](#實現方法4)。 **不同點2為是否有把最後終止字符儲存至字串陣列** : - 前者是**字串比較**,承如[lab1實現方法](#實現方法1),只會以`char`型態儲存輸入之數值,最後不會儲存終止字符。 ```cpp if (strcmp(input_value, password) == 0) { Serial.println("Unlock!"); } ``` - 後者為**整數比較**,最後會儲存字符,才能進行`atoi()`的正確轉換。<br>使用整數的目的為 1. 配合`long random(min, max)`回傳值的**資料型態**。 2. 方便調用我寫的`7-seg display`**這個`class`成員函數的接口**`void SHOW_SEG::show(int num);`。 ```cpp if (verification_code == atoi(input_value)) { Serial.println("Correct!"); } ``` ## lab2-2 ### SPEC 使用keypad輸入一個三位數字,實現**終極密碼** $\text{answer} \in [0, 999]$。 ### 實現方法 ```cpp while (true) { //4-digit number enter = keypad.getKey(); if (enter != NO_KEY) { //input guess_ch[count++] = enter; if (isDigit(enter)) { Serial.print(enter); } else if (enter == 'A') { break; } } } ``` - 第一層`if`判斷檢查是否輸入數值,內建`NO_KEY`為按鍵沒有被按下的值。 - 第二層`if`判斷輸入的合法性(validity),限定為輸入的資料型態為數字。 ```cpp guess = atoi(guess_ch); ``` 由於在前一程式最後按下`A`輸出時,會把字符`'A'`存進輸入數值的最後,在這邊起到**辨識數值長度**的功能,因為`atoi()`遇到**非數字**或**字符串結束('/0')** 時會結束轉換,並將結果返回。 ## lab3 ### SPEC 利用`MPU-6050`做出**計步器**功能。(只使用三軸加速度($a_x, a_y, a_z$)之值來進行運算,代表只有判斷旋轉角度) ## 觀念 `MPU6050`是一種非常流行的空間運動傳感器芯片,可以獲取器件當前的**3個加速度分量**和**3個旋轉角速度**。由於其體積小巧,功能強大,精度較高,不僅被廣泛應用於工業,同時也是航模愛好者的神器,被安裝在各類飛行器上馳騁藍天。 自帶了一個**數據處理子模塊DMP**,已經內置了濾波算法。數據接口用的是$\rm I^2C$(讀音 : I-squared-C)總線協議([原理](https://www.cntofu.com/book/46/arduino/i2c_protocol_yuan_li_ji_ying_yong.md))。 數據寫入和讀出均通過其IC內部的**寄存器**實現,這些寄存器的地址都是1個byte,也就是8位的尋址空間。 - 詳細資訊請參閱[MPU-6050 datasheet](https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf)。 ### 實現方法 1. **正規化**<br> ```cpp int sensitivity = 32767 / 250; ``` 當$G_X$取正最大值$32768$時,當前角速度為順時針$250\;\omega/s$,故可得靈敏度,即為**角速度1秒內1弧度**所代表的數值為 $$ \frac{32767}{250\;\omega/s} \tag{1.1} $$ 2. 將加速度數據代入回推**變化角度**<br> $$\tan^{-1}\Bigg[\; \frac{A_x} { \sqrt{(A_y)^2 + (A_z)^2} }\;\Bigg] \times \frac{180}{\pi} \tag{2.1}$$ $$\tan^{-1}\Bigg[\;\frac{A_y} { \sqrt{(A_x)^2 + (A_z)^2} }\;\Bigg] \times \frac{180}{\pi} \tag{2.2}$$ $$\tan^{-1}\Bigg[\;\frac{ \sqrt{(A_x)^2 + (A_y)^2} }{A_z} \;\Bigg] \times \frac{180}{\pi} \tag{2.3}$$ 3. 在`50`筆資料內,**每測量一次更新最小與最大值**,並相減。<br> 4. 最後檢查**是否超出臨界值**,條件為 : $$(θ_x)^2 + (θ_y)^2 + (θ_z)^2 < (\text{threshold})^2 \tag{4.1}$$ ## lab4 ### SPEC 承上題,**加上七段顯示器顯示**。 ### 實現方法 延續lab3程式 1. 觀察題目要求增加步數之數列為: $$0 + 1 + 8 + 27 + ...... +\; n^3 = \sum_{k=0}^{n} k^{3} \tag{1.1}$$ 2. 為簡化程式碼,使用`OOP`觀念,將`4-digit 7-segment display`驅動封裝成一個`class`,因此在主函數只需要增加2行 : ```cpp SHOW_SEG::SHOW_SEG(int first_pin, int last_pin) ``` 創建類對象,並調用**有參構造函數**,第一至第二參數為七段顯示器連續接口,即 $\text{pin} \in [2, 13]$。 ```cpp void SHOW_SEG::show(int num); ``` 調用**這個類的成員函數`show()`**,外部使用者只需要輸入一個整數,即可使七段顯示器亮燈,不需知道內部是如何實現,實現函數封裝。 ## 課後習題 ### Question 1 請說明非同步傳輸中的`RS-232`, `RS-422`以及`RS-485`,並討論這三者的差別為何? ### Answer 1 Arduino藉由`UART`作為資料傳輸介面,其線路僅有傳輸(TX)、接收(RX)兩個,以邏輯電路的形式傳輸資料,高電位時為1、零電位時為0。在Arduino要與PC溝通時,**由於PC沒有`UART`的傳輸介面**,則需要以下傳輸介面作為轉換,其差異如下: |傳輸介面|說明| |:-:|:-:| |**RS-232**|點對點通訊,傳送和接收端共用一個接地電位,並用**一根訊號線上的電壓**來判斷傳送的狀態爲0或1,容易引起干擾。| |**RS-422**|以**差動**的方式傳輸資料,兩根接收、兩根傳輸,因此可以同時發送與接收信號,即:**全雙工通信(full-duplex)**。藉由差動的方式可以減少雜訊干擾,可用於遠距離傳輸。| |**RS-485**|與`RS-422`差動傳輸原理相同,差別在於**只有兩根線同時作為傳輸、接收使用**,因此無法同時發送與接收信號,故只能使用**半雙工通信(half-duplex)**。因為僅需兩條線就可傳輸,適用於佈線成本高但是資料傳輸量需求不高的應用中。| ![](https://i.imgur.com/D8bJ5EE.png)| - 各傳輸介面之驅動電壓 |傳輸介面|邏輯0|邏輯1| |:-:|-:|-:| |**UART**|`0` V|`3.3` or `5` V| |**RS-232**|`3`~`15` V|`-3`~`-15` V| |**RS-422**|`-2`~`-6` V|`2`~`6` V| |**RS-485**|`-1.5`~`-6` V|`1.5`~`6` V| - `UART`和`RS-232C`之間要如何轉換?<br> 透過一顆`RS-232C`的傳送/接收器叫做`MAX232`,內部設計2個非常巧妙的電路 - **charge pump升壓**和**反向電路**,儘管只有這顆IC只有`5V`供電,還能輸入`RS-232`的正負電壓與高於`5V`之電壓。以下2個電路在`MAX232`內部是由4個電容器和一系列的開關切換電路自動完成 : 1. **Charge pump** |原始|後來| |:-:|:-:| |兩個電容器`C1`和`C2`,分別用`5V`電源將它們充飽|讓`C1`和`C2`脫離電源,並串聯起來| |![](https://i.imgur.com/kKl1x8m.png)|![](https://i.imgur.com/XCWHHP0.png)| 2. **反向電路** |原始|後來| |:-:|:-:| |一個電容器充電|將它的極性對調,讓原來電位比較高的那支腳與系統的`0 V`電位相連,由於電容器裡面的電位差不會改變,所以原來低電位那隻腳,現在相對於`0V`來說就是負電壓| |![](https://i.imgur.com/IQdnEI6.png)|![](https://i.imgur.com/XCWHHP0.png)| - ref : [【Maker電子學】認識UART界面#1—訊號格式](https://makerpro.cc/2019/12/uart-part-1/) ### Question 2 `MPU-6050`的接腳`XDA`、`XCL`、`AD0`功能為何? ### Answer 2 |接腳|介紹| |:-:|:-| |`VCC`|提供`MPU-6050`系統電壓,3~5V皆可。| |`GND`|`MPU-6050`系統接地。| |`SCL`|$\rm I^2C$通訊協定所需的**時脈週期**。| |`SDA`|藉由$\rm I^2C$通訊協定**傳輸資料**。| |`XDA`|以`MPU-6050`為master、額外模組為slave,**傳輸資料給額外模組**。| |`XCL`|以`MPU-6050`為master、額外模組為slave,**提供時脈週期給額外模組**。| |`AD0`|當有兩個`MPU-6050`連接在同一個master(arduino board)上時,須分別給兩個slave(`MPU-6050`)**不同通訊位址**。 `LOW`->`0x68`、`HIGH`->`0x69`,可自行選擇其通訊位置。| |`INT`|中斷`MPU-6050`計算程序,**使arduino可讀取其資料**,以避免`MPU-6050`資料溢位。| ### Question 3 請問取`random`數字時不使用`randomSeed()`的差別在哪? ### Answer 3 - **`random(max), random(min, max)`**<br> 生成隨機亂數。 - **`randomSeed(unsigned long seed)`**<br> 初始化偽隨機數生成器,使其從隨機序列中的任意點開始。生成序列取決於種子碼的值,因此都是一樣的,若要不同,則需指定一個亂數種子。 - **舉例說明**<br> 以lab2-1、lab2-2為例,使用`randomseed()`時,其seed的來源為未連接的pin腳`analogRead(0)`所讀取到的值,由於該腳位為連線,因此會受其他因素(其他模擬輸入的值、手與電路板的距離等)影響。 > 此觀念與`C/C++`相同。 ### Question 4 下方為不呼叫`MPU-6050 library`來讀出`MPU-6050`數值之程式, 請為其標上完整的註解。 ### Answer 4 ```cpp #include<Wire.h> ``` - **函式庫引用**<br> `MPU6050`使用$\rm I^2C$(讀音 : I-squared-C)總線協議([原理](https://www.cntofu.com/book/46/arduino/i2c_protocol_yuan_li_ji_ying_yong.md)),因此我們需要頭文件`<Wire.h>`來實現Arduino與`MPU6050`之間的傳輸。 ```cpp const int MPU_addr=0x68; //在程式執行開始時會執行一次,用於初始化設定 void setup(){ Wire.begin(); //初始化Wire.h並作為master or slave加入I2C bus //如未指定7-bit slave地址(函數重載),則為加入者為master Wire.beginTransmission(MPU_addr); //開啟MPU6050的傳輸 Wire.write(0x6B); //指定寄存器地址 Wire.write(0); //寫入一個字節的數據 Wire.endTransmission(true); //結束傳輸,true表示釋放總線 ``` - **將數據寫入MPU-6050**<br> 在每次向器件寫入數據前要先打開`Wire`的傳輸模式,並指定器件的總線地址,`MPU6050`的總線地址是`0x68`(`AD0`引腳為高電平時地址為`0x69`),地址可於IC的datasheet中查閱或[自己寫i2c_scanner檢測](https://www.makerguides.com/character-i2c-lcd-arduino-tutorial/)。然後**寫入一個字節的寄存器起始地址,再寫入任意長度的數據**。<br><br> 這些數據將被連續地寫入到指定的起始地址中,超過當前寄存器長度的將寫入到後面地址的寄存器中。寫入完成後關閉`Wire`的傳輸模式。示例是向`MPU6050`的`0x6B`寄存器寫入一個byte`0`。 ```cpp Serial.begin(38400); //設定傳輸時的baud(每秒有幾個bit) } //重複執行在loop()函式中的程式碼,直到Arduino硬體關閉 void loop(){ Wire.beginTransmission(MPU_addr); //開啟MPU6050的傳輸 Wire.write(0x3B); //指定寄存器地址 Wire.endTransmission(false); //開啟傳輸模式 //If false, endTransmission() sends a //restart message after transmission. //The bus will not be released, //which prevents another master device //from transmitting between messages. Wire.requestFrom(MPU_addr,14,true); //從MPU要求14byte資料 AcX=Wire.read()<<8|Wire.read(); //兩個字節組成一個16位整數 AcY=Wire.read()<<8|Wire.read(); AcZ=Wire.read()<<8|Wire.read(); //Tmp=Wire.read()<<8|Wire.read(); //題目少一行,與溫度有關之數值 GyX=Wire.read()<<8|Wire.read(); GyY=Wire.read()<<8|Wire.read(); GyZ=Wire.read()<<8|Wire.read(); ``` - **從MPU-6050讀出數據**<br> 讀出和寫入一樣,要先打開`Wire`的傳輸模式,然後寫一個字節的寄存器起始地址。接下來將指定地址的數據讀到`Wire.h`的緩存中,並關閉傳輸模式,最後從緩存中讀取數據。<br><br> 示例是從`MPU6050`的`0x3B`寄存器開始讀取`14`個byte的數據。由於[`Wire.read()`回傳值](https://www.arduino.cc/en/Reference/WireRead)為**下一個接收到byte**,總共`14`個`Wire.read()`,因此master才會要跟slave要`14`個byte。依序兩兩一組,先讀到的A向左位移`8`bit,後讀到的B與A做`Bitwise OR`因此等同於2個`8`個bit整數concatenate成一個`16`個bit整數。<br><br>舉例來說 $$ \begin{array}{clcr} \text{字串A} & 1 & 0 & 0 & 1 & 1 & 1 & 1 & 1\\ \text{字串A left shift 8 bits} & 1 & 0 & 0 & 1 & 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\ \text{字串B} & & & & & & & & & 1 & 0 & 1 & 0 & 0 & 1 & 0 & 1\\ \text{與左移之字串A做Bitwise OR} & 1 & 0 & 0 & 1 & 1 & 1 & 1 & 1 & 1 & 0 & 1 & 0 & 0 & 1 & 0 & 1\\ \end{array} $$ ```cpp Serial.print("AcX = "); Serial.print(AcX); //傳輸並顯示在serial monitor Serial.print(" | AcY = "); Serial.print(AcY); Serial.print(" | AcZ = "); Serial.print(AcZ); Serial.print(" | GyX = "); Serial.print(GyX); Serial.print(" | GyY = "); Serial.print(GyY); Serial.print(" | GyZ = "); Serial.println(GyZ); delay(333); //延遲333ms } ``` - **印出數據**<br> 將結果透過TX腳位並藉由之前所設定之baud的傳輸速率,傳到電腦並顯示在serial monitor上。 - **參考資料**<br> 1. [Arduino and MPU6050 Accelerometer and Gyroscope Tutorial](https://howtomechatronics.com/tutorials/arduino/arduino-and-mpu6050-accelerometer-and-gyroscope-tutorial/) 2. [MPU6050的數據獲取、分析與處理](https://www.itread01.com/content/1507437608.html#:~:text=MPU6050%E6%98%AF%E4%B8%80%E7%A8%AE%E9%9D%9E%E5%B8%B8%E6%B5%81%E8%A1%8C,%E9%A1%9E%E9%A3%9B%E8%A1%8C%E5%99%A8%E4%B8%8A%E9%A6%B3%E9%A8%81%E8%97%8D%E5%A4%A9%E3%80%82) 3. [EduCake 的 I2C 通訊](https://www.86duino.com/wp-includes/file/Chapter06-TC.pdf) 4. [認識UART界面](https://makerpro.cc/2019/12/uart-part-1/) 5. [How I2C Communication Works and How To Use It with Arduino](https://www.youtube.com/watch?v=6IAkYpmA1DQ) ## 心得 ### 劉永勝 這次實驗還是沒能在時間內完成,在lab2-1的地方,原先我們使用`switch`語法,增加兩個新功能,但在燒入Arduino時,始終**無法操作keypad**,搜尋了許多資料,最後只好改成`if-else`語法才終於通過,其問題原因不明。 而在取`MPU-6050`之數值時,我認為取角度來作計步器的條件不太好,計步器應該是要接受到震動再作用,所以若**用加速度可能比較符合實際計步器**。 ### 李宇洋 此次實驗已經較為熟悉Arduino的操作方式以及接線方法,但是因為程式端的問題,所以我們組別還是超過上課時間後才把demo做完。這次主要卡在lab2-1**新密碼字串處理**的問題以及`switch`裡的`default`不知為何無法被Arduino IDE正確讀取到,因為被`switch`語法折騰了很久,所以我們後來乾脆改用`if-else`語法,才終於解決這部分的問題。 而處理字串的問題礙於時間關係因此沒有完善的處理好,本來的想法是利用`string.length()`將每次輸入的新密碼長度記住後再將buffer清空,並重複每一次新密碼的輸入,但是不知為何會一直讀取到空字元,希望下次再次遇到類似問題時能順利解決。 ### 陳旭祺 lab1、lab2-1、lab2-2我傾向於**不使用**`string`與`password`這兩個class的調用,而使用傳統的`C-style string`進行字串處理,以方便操作底層。 而lab3、lab4使用`MPU-6050`做出**計步器**功能,這部分重點在於取50點數據中最小與最大值,並相減與臨界值進行比對。我認為題目限定判斷條件只用旋轉角度,而不使用加速度,不是一個好的設計,因為平常在跑步時,除非計步器沒有固定好在腰上,不然**計步器是隨人的身體上下移動,而比較少會有大幅度旋轉的運動軌跡**。 另外,以下記錄我遇到的問題與解決方法 : |項目|說明| |:-:|:-:| |問題|Arduino第一次`Serial.prtint`常常出現亂碼、錯誤縮排。| |推測原因(有待商榷)|用[stdin的觀念](https://www.howtogeek.com/435903/what-are-stdin-stdout-and-stderr-on-linux/)思考,緩衝區一開始未清空字符,印出不預期之空格。 |解決方法|在設定鮑率`Serial.begin()`前面加上`Serial.println();`<br>先自行flush掉緩衝區內的字元。|