在上一篇文章(Linkit 7697 + IMU 10DOF v2.0 研讀紀錄(一)),我們發現了I2Cdev:readBits()
這個函式沒有按照正確的結果輸出,因為止不住好奇心所以再深入分析一下這個程式。
原始程式碼
/** Read multiple bits from an 8-bit device register.
* @param devAddr I2C slave device address
* @param regAddr Register regAddr to read from
* @param bitStart First bit position to read (0-7)
* @param length Number of bits to read (not more than 8)
* @param data Container for right-aligned value (i.e. '101' read from any bitStart position will equal 0x05)
* @param timeout Optional read timeout in milliseconds (0 to disable, leave off to use default class value in I2Cdev::readTimeout)
* @return Status of read operation (true = success)
*/
int8_t I2Cdev::readBits(uint8_t devAddr, uint8_t regAddr, uint8_t bitStart, uint8_t length, uint8_t *data, uint16_t timeout) {
// 01101001 read byte
// 76543210 bit numbers
// xxx args: bitStart=4, length=3
// 010 masked
// -> 010 shifted
uint8_t count, b;
if ((count = readByte(devAddr, regAddr, &b, timeout)) != 0) {
uint8_t mask = ((1 << length) - 1) << (bitStart - length + 1);
b &= mask;
b >>= (bitStart - length + 1);
*data = b;
}
return count;
}
它原本的意思是將一個byte裡面的幾個bit讀出來,因為最小資料儲存單位是byte,所以讀出來的資料要以byte的方式呈現,譬如資料是01101001,如果我們想從第4個bit開始讀三個位元的資料,意思是從第4個bit處往右讀三個位元。最後的輸出應該是00000010。這裡的LSB是第0位元,MSB是第7位元,bitStart = 4, length = 3。
要達成這個效果,首先需要製作一個遮罩,遮罩的程式碼是在line 18。
uint8_t mask = ((1 << length) - 1) << (bitStart - length + 1);
把數字帶進去會得到mask = ((1 << 3) – 1) << (4 – 3 + 1),等於00011100(2)。
將數字和遮罩一起經過&運算後會得到00001000。
最後在經過 b >> (bitStart -length +1),就是 b >> (4-3+1)會得到00000010作為最後的輸出。
可是這個程式沒有執行錯誤檢查,當length > bitStart時就有可能會輸出錯誤的結果。按照嵌入式系統設計師的思維,會寫這些底層code的都是高手,應該不會有人犯下把length > bitStart當作輸入參數的低級錯誤吧。但是MPU9250的開發者就犯下了這個錯誤,它可能用高階語言的思維,以為系統對於length > bitStart的部分會有錯誤檢查與校正。
PS: 為什麼I2Cdev::readBites()
不做錯誤檢查與校正呢? 因為那些程式碼會浪費掉系統過多的資源,在嵌入系統中有效率的使用系統資源是需要優先被考慮的
細節分析
另外可能造成錯誤的原因是Compiler對於型別轉換的處理以及對於運算子的處理有不同的解讀所造成的。我以前教C的時候就曾經提出過:「千萬不要將型別轉換交給Compiler處理」。
這裡使用的uint8_t就等於byte,也等於unsigned char。是由0~255的數字組成,沒有正負符號。然而系統在進行運算的時候通常會使用最方便的單位來運算,像是32位元電腦,他的一次運算單位就是32bits,你要處理8位元的運算時,他還是以32位元作為單位來運算,算完以後再裁切成8位元。在這個過程中也容易出現錯誤,尤其是嵌入式系統的設計常常處理8位元資料。
第二個問題是,當位移運算子「<<」在處理運算時,如果右運算元為負值,則Compiler可能的運算會變成怎樣?
有兩個狀況,1. 會將右運算元改成正值,同時將「<<」運算改成「>>」運算;2. 忽略不運算。這裡就有了不確定性,在寫程式的時候需要避免你的程式碼產生這樣的問題。
關於Linkit 7697會是怎樣的輸出? 我們寫一個測試程式來檢驗Compiler的輸出
void setup() {
uint8_t b = 0x71;
uint8_t bitStart=6, length=8;
uint8_t i = bitStart - length + 1;
uint8_t mask = ((1 << length) - 1) << (bitStart - length + 1);
uint8_t mask2 =(( 1 << length) - 1) << i;
Serial.begin(38400);
Serial.print("\n直接運算:b << 1 = 0x");
Serial.print(b << 1, HEX);
Serial.print("; b << -1 = 0x");
Serial.println(b << -1, HEX);
Serial.print("使用代數運算:mask = 0x");
Serial.print(mask, HEX);
Serial.print("; mask2 = 0x");
Serial.println(mask2, HEX);
Serial.println("變數型別與使用空間判斷:");
Serial.print("(bitStart - length + 1) = 0x");
Serial.print((bitStart - length + 1), HEX);
Serial.print(" = ");
Serial.print((bitStart - length + 1), DEC);
Serial.print("; sizeof (bitStart - length + 1) = ");
Serial.print(sizeof (bitStart - length + 1));
Serial.println("bytes");
Serial.print("(6 - 8 + 1) = 0x");
Serial.print(6 - 8 + 1, HEX);
Serial.print(" = ");
Serial.print(6 - 8 + 1, DEC);
Serial.print("; sizeof (6 - 8 + 1) = ");
Serial.print(sizeof (6 - 8 + 1));
Serial.println("bytes");
Serial.print("i = 0x");
Serial.print(i, HEX);
Serial.print(" = ");
Serial.print(i, DEC);
Serial.print("; sizeof (i) = ");
Serial.print(sizeof i);
Serial.println("bytes");
}
void loop() {
// put your main code here, to run repeatedly:
}
輸出結果
程式碼line 9~12為直接運算,很顯然當 b << -1 時,Compiler自動將運算子改成 b >> 1 來進行運算,所以得到0x38的結果。
程式碼line 5,讓系統自動轉換運算後型別,雖然運算式子中bitStart和length的型別都是uint8_t,但是系統將1認定為int,最後轉出來的型別為int,這是有帶符號的,所以運算結果是-1。
line 6,我們先定義一個uint8_t作為容器承接運算後的結果,雖然運算後的結果是 -1,但經過型別轉換變成沒有正負符號的255了,所以這個運算式就變成了<< 255,向左移動255次,那結果自然為0。
line19~40寫一些我使用的判斷法則,用來判斷變數的型別。
仍未結束…
原本以為已將掌握到規律了,最後我們將這些測試碼拷貝到I2Cdev::readBits()這個程式裡面來測試
code:
int8_t I2Cdev::readBits(uint8_t devAddr, uint8_t regAddr, uint8_t bitStart, uint8_t length, uint8_t *data, uint16_t timeout) {
// 01101001 read byte
// 76543210 bit numbers
// xxx args: bitStart=4, length=3
// 010 masked
// -> 010 shifted
uint8_t count, b;
if ((count = readByte(devAddr, regAddr, &b, timeout)) != 0) {
#ifdef I2CDEV_SERIAL_DEBUG
Serial.println("\nI2Cdev::readBits()");
Serial.print("bitStart = ");
Serial.print(bitStart, DEC);
Serial.print("; length = ");
Serial.print(length, DEC);
Serial.print("; b (before) = 0x");
Serial.print(b, HEX);
#endif
uint8_t mask = ((1 << length) - 1) << (bitStart - length + 1);
uint8_t i = -1;
uint8_t mask2 = ((1 << length) - 1) << i;
b &= mask;
b >>= (bitStart - length + 1);
*data = b;
#ifdef I2CDEV_SERIAL_DEBUG
Serial.print("; mask = 0x");
Serial.print(mask, HEX);
Serial.print("; b (after) = 0x");
Serial.println(b, HEX);
Serial.println("testing for data format:");
Serial.print("mask2 = 0x");
Serial.println(mask2, HEX);
b = 0x71;
Serial.print("\n直接運算:b << 1 = 0x");
Serial.print(b << 1, HEX);
Serial.print("; b << -1 = 0x");
Serial.println(b << -1, HEX);
Serial.print("使用代數運算:mask = 0x");
Serial.print(mask, HEX);
Serial.print("; mask2 = 0x");
Serial.println(mask2, HEX);
Serial.println("變數型別與使用空間判斷:");
Serial.print("(bitStart - length + 1) = 0x");
Serial.print((bitStart - length + 1), HEX);
Serial.print(" = ");
Serial.print((bitStart - length + 1), DEC);
Serial.print("; sizeof (bitStart - length + 1) = ");
Serial.print(sizeof (bitStart - length + 1));
Serial.println("bytes");
Serial.print("(6 - 8 + 1) = 0x");
Serial.print(6 - 8 + 1, HEX);
Serial.print(" = ");
Serial.print(6 - 8 + 1, DEC);
Serial.print("; sizeof (6 - 8 + 1) = ");
Serial.print(sizeof (6 - 8 + 1));
Serial.println("bytes");
Serial.print("i = 0x");
Serial.print(i, HEX);
Serial.print(" = ");
Serial.print(i, DEC);
Serial.print("; sizeof (i) = ");
Serial.print(sizeof i);
Serial.println("bytes");
#endif
}
return count;
}
輸出結果
發現如果把相同的程式碼放到library中 b << -1 = 0xo,而且mask = 0x0。意思是compiler對於「<< -1」又有不同的解釋。這邊糾結的問題是同一隻程式碼在同一個晶片上執行應該要有相同的結果才是,持續探討…。以上都是我以前學習過也碰到過的狀況,以下則會有學習到新東西的可能。