前言,和 DataQ 的初相遇

大概在20年前左右,筆者其實在一家生產離子佈值機的廠商中任職。記得有一次,被客戶緊急呼叫,跟客戶的製程整合部門開會。他們發現,他們的生產出來的產品中,最後會有嚴重的失效狀況。但是頻率不是非常高的,而是數千片晶圓中偶而出現一片。經過電性分析後,懷疑到離子佈值這一站製程中,有一道被稱之為叫 LDD (Lightly-Doped Drain) 離子佈值來囉。

所謂的 LDD 離子佈值,大家應該都知道,邏輯IC主要的製程是在做 MOSFET。 MOSFET結構是個 Gate-Oxide 所組成的電容器,隨著 Gate-Oxide 越來越小,Gate-Oxde 的兩端,在操作的時候電場會因為邊際效應越來越強。那個年代的解法就是在 gate-oxide 的側邊,輕微的參雜三價或五價的 Dopant,來降低 gate-oxide 兩旁的電場效應,所以被稱之為 LDD (Lightly-Doped Drain)製程。

製程參雜的方法,如果用一般的方式先做 spacer 的話又太複雜囉。所幸,中電流離子佈值機的晶圓製程承載盤(Platen)是設計成可以有任意傾斜角(Tilt Angle),及可以轉動的。所以只要以高傾斜角,就可以從 gate-oxide 的側邊植入(參雜)一定劑量的離子,造成 Lightly-Doped。然後再轉動90度,從另外一個側邊再植入(參雜)一定劑量的離子,... 連轉四次,就可以輕易地完成四個側邊的 LDD 參雜的製程。

那次,經過產品電性故障分析的結果,客戶的製程整合高度懷疑這個四次的轉動,少轉了一次。但是很奇怪的是,這並不是每次都發生的事,約萬次的LDD離子佈值製程,才有機會發生一次。

這真的是非常非常奇怪的一件事,因為那個旋轉的馬達是直接透過齒輪帶動晶圓製程承載盤(Platen)旋轉的,透過精密的光學尺(Encoder)來回授控制運動,馬達稍微失控就會立刻被知道的。絕不可能故障了,一會兒轉動,一會兒又不轉動的。

為了解決這個奇怪問題,母公司從國外寄來一個叫做 DataQ Starter Kit 的東東,是個 DataQ 這家公司為了初學者所開發的一個東東。因是初學者套件 Starter Kit ,所以價格也非常之便宜,大概台幣三千塊左右。原來是個兩個頻道的類比轉數位的 ADC USB 裝置。它可以把 0 - 5V 的類比電壓,以兩個獨立頻道讀入,轉成 0 - 1024 (10bits) 的數位讀值後透過 USB 傳給 NoteBook 上的專屬軟體做紀錄,並也可以輸出成CSV檔案提供後續分析之用。

就這樣,大家就把 DataQ 跟NoteBook電腦掛上Platen馬達控制器裡頭電路板特定位置的監控點,透過控制電路的訊號監控Platen馬達旋轉的狀況,是不是每次都真的有轉了四次呢?

監控好幾天後,透過分析 DataQ 所錄到的資料,最後 Bingo!難以致信的是,真的抓到有馬達沒有轉的狀況。真的是少轉了一次,罪證切鑿啊!

顯然,馬達控制器是好的,馬達也是好的,所以最後萬夫所指的回到機台的主控軟體。透過軟體工程師仔細的 debug 控制程式碼的狀況後,最後終於發現原因了。原來問題來自於程式裡面紀錄轉動次數的變數的四捨五入誤差。當轉動到萬次後,這個誤差有機會多跑一次出來而讓軟體誤以為已經轉了,因而少轉一次釀成大禍。

最後,透過軟體的修正,這個問題順利地被解決了。自然,當時對 DataQ 這個能夠解決這麼棘手問題的工具,印象深刻!

 

Arduino 迷你DataQ資料記錄器

現在單晶片微處理器這麼發達的時代,無需任何外掛,我們的 Arduino Uno 裡的 Atmega328p 內部也直接內建一個 ADC 單元,透過內建的多工器,最多可以擴充到6個類比輸入。所以讓我們也來做一個迷你 DataQ 吧!相信就像上面的例子,會成為未來非常重要的得力工具的。

而且,更進一步, Atmega328 內建 SPI 介面,所以是可以直接驅動 SD 卡的寫入跟讀取喲。所以直接可以建立一個比當年 DataQ 更先進的,無需電腦或 NoteBook,可以直接把類比轉數位輸入的資料寫入SD卡成為CSV的檔案喲。

然後,很開心的,其實 Arduino 的資源真的不是蓋的,市面上已經有現成幫你準備好,以 SPI 當介面, Arduino Shield 形式的 SD卡讀寫的硬體囉,買來就立刻可以使用囉。更讚的是,上面還善用了 Atmega328 的 i2c 介面,直接掛了一顆以 i2c 運作的 DS1307 即時時鐘。這樣連時間都沒問題囉。

 

程式功能規劃

因為要脫離電腦使用,然後我還是不喜歡掛 LCD 螢幕啦。喜歡直接一點,只用 LED發光二極體跟按鈕來做選擇跟控制囉。

所以,功能設計如下

1. 雖然 Arduino Uno 有6個ADC 頻道,但因為i2c用掉其中的 A4, A5兩個腳位,所以只剩4個 Channel 的 ADC 可以使用 (A0, A1, A2, A3)

2. 預計有6個ADC取樣頻率的選擇跟指示燈:只提供 100Hz, 50Hz, 20Hz, 10Hz, 1Hz, 0.2Hz 這六種比較常用的取樣頻率。透過一個 Freq 頻率選擇按鈕來進行頻率的選擇跟切換。

3. 有一個 Start/Stop 按鈕來啟動紀錄開始,跟紀錄結束。當按一次按鈕紀錄開始時對應取樣頻率的LED會開始閃爍,提醒使用者正在進行資料紀錄。再按一次按鈕紀錄結束時LED恢復恆亮,提醒使用者紀錄已經停止。

4. 當程式檢查到使用者忘記插入 SD卡,此時6個LED會同時開始閃爍,提醒使用者裡面沒有SD卡片。

總結就是,兩個按鈕:一個 Start/Stop按鈕,一個 Freq按鈕。6個指示不同頻率的 LED。4個ADC輸入。

 

硬體線路規劃

利用買來的附有即時時鐘的 SD卡 Shield,就可以立刻做資料記錄處理囉。至於按鈕跟LED顯示,經過考慮,決定再疊上另一層的 Arduino Shield 板,像積木一樣,這樣可以發揮最大的使用彈性。按鈕跟LED就焊接在這個空白 Shield 上面囉。

買來現成的 Arduino SD 卡 Shield

1ED12D4A-9949-47EE-AE3A-A66B4651062E.jpeg

 

上面疊一個自製按鈕跟 LED 人機使用介面 Shield,先用麵包板測試電路沒接錯

50F5B202-84D2-4CDB-B514-AA2B85F29E70.jpeg

 

 

硬體線路如下

1C4AD175-A75A-44EE-97DE-DA066CDBDF2A.jpeg

麵包板測試時的照片

022C2886-5F98-460C-BA4D-A99041B222BE.jpeg

 

 

實際焊接完成品的照片

070D9FB8-B7FB-4AB8-9F81-EEE91B14B284.jpeg

 

9F2F03C3-BF6C-4582-98C0-ABF82533D0F4.jpeg

 

9B8F76CA-DED3-4422-B4B0-F5E85FC12684.jpeg

 

978DBBFD-F7B8-4404-AAE0-8C396F3B79B8.jpeg

 

 

Arduino SD 卡資料讀寫

這篇其實主要的內容就是在練習如何利用 Arduino 來控制 SD 卡控制器,並對其寫入檔案資料。

這裡顯現出 Arduino 的威力囉,官方已經寫好囉,我們一行控制 SPI 的 SD卡控制器的程式碼都不用寫。為了最大的通用性,我們選用官方所提供的標準SD卡控制函式庫呀。

這個函式庫就存在 SD.h 裡面,然後它是利用 SPI 來運作的,所以 SPI.h 這個也必須被包含進來,所以使用第一步

#include <SPI.h>

#include <SD.h>

用法其實跟一般的檔案操作大同小異,就 open() 檔案,print()寫入資料,close()檔案,這樣資料就進去囉。精確步驟如下

1. 啟動 SD控制

SD.begin(SPI_CS) 啟動SD卡控制

SPI_CS 是 SPI ChipSelect pin 腳位置。UNO 的話是 Pin 10.

假如成功啟動,且 SD插槽裡面有SD卡片的話,回傳 true,否則回傳 false。

 

2. 產生個檔案物件進行操作

File dataFile; 產生檔案型別物件

 

3. 檔案開啟

dataFile = SD.open(filename, FILE_WRITE);

開啟檔案,並將回傳存入檔案型別。

open() 裡面要提供兩個參數,第一個參數是檔案名稱的 char * 字串位址。第二個參數是要執行的操作模式 mode:"FILE_READ" 就是讀取,"FILE_WRITE" 則是寫入檔案。開啟檔案後,它會回傳一個真正的檔案物件回來,我們要把它存入剛剛的File檔案型別物件方便後續的操作。

 

4. 寫入資料

透過 print(data) 來寫入資料, data 可以是字串,byte,long,int

dataFile.print(data);

如果要加上換行,可以用 println(data)

dataFile.println();

 

5. 最後,檔案關閉來結束

關閉檔案,close() 來結束檔案讀寫。

要留意的,一次只能操作一個檔案,要操作另一個檔案須先利用這個指令來關閉目前正在操作的檔案才可以開啟另一個新檔案。

dataFile.close();

 

DS1307 即時時鐘控制

我買到的 Arduino SD卡,上面是有個 DS1307 的即時時鐘的。這個即時時鐘利用 i2c 來溝通。因為使用方式還蠻簡單的,所以沒有使用別人的函式庫,而是自己寫。細節請看我的上一篇 BLOG,這裡就不贅述囉。這裡只是把上一篇 BLOG 的程式碼放進來而已。

 

按鈕的處理

在這邊的這個應用中,我們有兩個按鈕。一個控制紀錄的 start/stop,另一個控制頻率的切換。

當程式忙著從 ADC讀資料進來,然後透過 SPI 控制 SD卡控制器進行檔案資料的寫入時,其實已經蠻忙碌的。假如對這兩個按鈕採用輪詢 Polling 的方式來運作的話,反應性會很差。所以我喜歡用中斷的方式。剛好 Arduino Uno 有兩個中斷腳位,恰好可以分配給這兩個按鈕。

中斷函式跟中斷向量的連結,由指令 attachInterrupt(pin, func, mode) 來達成

 // attach ISR for two buttons  

attachInterrupt(digitalPinToInterrupt(freq_btn),freq_sense,FALLING);  

attachInterrupt(digitalPinToInterrupt(start_btn),start_sense,FALLING);

這個 attachInterrupt() 需要三個參數

第一個參數是中斷 PIN 腳的位置編號

要解說一下的,中斷 PIN腳的編號跟 Arduino 真實 PIN腳的編號並不一樣。所以官方是建議利用 digitalPinToInterrupt(真實Pin腳) 的這個函式來做轉換。

第二個參數是中斷處理函數的名稱

第三個參數是觸發的模式:

FALLING = 負沿觸發(由上往下), RISING = 正沿觸發(由下往上), LOW = 訊號0V時觸發, HIGH = 訊號5V時觸發, CHANGE = 訊號改變時觸發。

這裡是要偵測按鈕按下,所以採用 FALLING。

 

偵測按鈕,一般需要上拉電阻。Atmega328 很棒的地方是,每個 Digital I/O 已經內建了。為了方便,我們當然採用內建的上拉電阻。

pinMode(start_btn, INPUT_PULLUP);  

pinMode(freq_btn, INPUT_PULLUP);

這裡的 INPUT_PULLUP 可以將對應的 Digital I/O PIN 腳改為輸入,並啟動內建的上拉電阻,非常的方便。

 

再來是按鈕彈跳的處理,這裡利用 millis() 來取得系統的時間,紀錄到 time1 或 time2 的變數中

當按鈕被按,中斷函數被呼叫執行,先透過 time1 跟 time2 的紀錄取得最後一次被按的的時間。假如最後被按的時間跟現在的時間小於 250 mS ,那肯定是因為按鈕彈跳所造成的假訊號,所以可以不用理會。大於 250mS 才是真訊號,所以進行對應的按鈕處理。

void freq_sense()  // freq按鈕處理 

{  if (millis() - time1 > 250)

   {    time1=millis(); 更新最後被按時間   

        這是真的,進行處理

   }  

}

 

程式簡介

用了三個函式庫

<SPI.h> SPI函式庫,這是透過SPI介面控制 SD卡讀取時必備的

<Wire.h> I2C函式庫,用來跟 DS1307 即時時鐘溝通用

<SD.h> Arduino 官方 SD 卡檔案管理跟讀寫函式庫

 

一些設定

#define SPI_CS 10    SD卡SPI介面 Chip Select 接腳的位置

#define Max_ADC 4    ADC 頻道的總數 

#define Max_Records 60000  一個檔案最多容納的資料筆數 

#define i2c_addr 0x68    RTC時鐘 i2c位址

#define tab "\t"     CSV檔案中的資料分隔字元 

#define freq_no 6     頻率選擇的總數 

 

int count = 0;      目前紀錄的 sample數目 

byte currFreq = 0;   目前使用者所做的頻率選擇 

bool isStart = false;    旗號,已經開始紀錄為 true 

bool isFileOpen = false;  旗號,檔案已經開啟為 true

bool toggle = false;    旗號,用來讓LED閃爍。目前是亮的為 true 

unsigned long delayTime=0; 用來延遲特定時間,維持頻率的準確 

TimeDate currTimeDate;    RTC 即時時鐘的時間

 

const unsigned long delayTable[freq_no] =  {7, 17, 47, 97, 997, 4997}; 這個陣列存放不同取樣頻率下,所需的延遲時間。用來控制取樣頻率的準確度。

例如 delayTable[3] = 97 mS, index=3 為頻率 10Hz.也就是說,整個程式從ADC0到ADC3 取樣完畢並完成紀錄到SD的動作,大概需要 3mS,所以需要額外再 Delay(97mS)來維持住每筆的資料間隔是準確的 100mS (10Hz) 

 

const byte LEDPins[freq_no] = { LED_100Hz, LED_50Hz, LED_20Hz, LED_10Hz, LED_1Hz, LED_p2Hz }; 紀錄 LED Pin腳資料的資訊,用陣列使用index來操作比較方便且有彈性當使用者點擊Freq按鈕時做頻率的切換,程式控制對應 LED的顯示動作。

 

const int ADCPins[Max_ADC] = { ADC0_Pin, ADC1_Pin, ADC2_Pin, ADC3_Pin }; 紀錄 Arduino ADC 的 Pin腳資料資訊,用陣列使用index來操作比較方便使用迴圈來對其操作,且更動Pin腳時也比較有彈性。

 

const char * freq_str[freq_no] = { "100Hz", "50Hz", "20Hz", "10Hz", "1Hz", "0.2Hz" } ; 序列埠顯示用,每按一次頻率按鈕假如有接電腦的終端機的話,顯示目前所選擇的頻率。

 

測試的狀況

測試影片

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Frank Lin(@ohiyooo)分享的貼文

 

有逐一地將 ADC0, ADC1, ADC2, ADC3 接上訊號產生器。錄一段後將檔案拿到電腦驗證。運作是OK的,所有功能測試無誤。

不過也發現 Arduino 的 ADC 的端子,如果不使用的話一定要接地。否則會強烈的受到干擾而得到錯誤的資料。例如假如我們只有一個資料源,當ADC0接上這個訊號源來監控時,其他的 ADC1, ADC2, ADC3 就ㄧ定接地,否則它們是浮接的狀態下會受到 ADC0 的訊號的干擾而呈現出假的訊號的狀態。

當初沒發現這個問題,不然每個頻道都應該要設計個接地開關的,沒訊號源輸入監控時就切到「接地」

 

最後,原始程式碼列表

 

//
// ADC Data Logger V1.0
//   2021.03.05 Frank Lin
//          


#include <SPI.h>
#include <Wire.h>
#include <SD.h>

#define SPI_CS 10           // SPI CS Pin
#define Max_ADC 4           // Max # of ADC
#define Max_Records 60000   // max # of records in one file
#define i2c_addr 0x68       // RTC I2C addr
#define tab "\t"            // tab delimited on data
#define freq_no 6           // max # of freq

// pin assignment

#define LED_100Hz 9
#define LED_50Hz  8
#define LED_20Hz  7
#define LED_10Hz  6
#define LED_1Hz   5
#define LED_p2Hz  4

#define freq_btn  3  // freq button
#define start_btn 2  // start button

#define ADC0_Pin A0
#define ADC1_Pin A1
#define ADC2_Pin A2
#define ADC3_Pin A3


typedef struct time_date {
  byte year;
  byte month;
  byte date;
  byte day;
  byte hours;
  byte mins;
  byte secs; 
} TimeDate;



// Data Structures

int count = 0;             // sample count
byte currFreq = 0;         // current freq user slected
bool isStart = false;      // is recording started?
bool isFileOpen = false;   // is file opened?
bool toggle = false;       // toggle for LED flashing
unsigned long delayTime=0; // delay time between new sample
TimeDate currTimeDate;     // RTC time data

// Frequency  Delay (mS)
// 100Hz         7
//  50Hz        17
//  20Hz        47
//  10Hz        97
//   1Hz       997
// 0.2Hz      4997

const unsigned long delayTable[freq_no] =  {7, 17, 47, 97, 997, 4997};
const byte LEDPins[freq_no] = { LED_100Hz, LED_50Hz, LED_20Hz, LED_10Hz, LED_1Hz, LED_p2Hz };
const int ADCPins[Max_ADC] = { ADC0_Pin, ADC1_Pin, ADC2_Pin, ADC3_Pin };
const char * freq_str[freq_no] = { "100Hz", "50Hz", "20Hz", "10Hz", "1Hz", "0.2Hz" } ;

unsigned long time1;  // timer for freq sense
unsigned long time2;  // timer for start/stop sense

void freq_sense()  // freq button sense
{
  if (millis() - time1 > 250)
  {
    time1=millis();
    currFreq++ ;
    if(currFreq==freq_no) currFreq=0;
    delayTime = delayTable[currFreq];
    Serial.println(freq_str[currFreq]);
    toggle=true;
    
    for (byte i=0; i<freq_no;i++) {
      if (i==currFreq)
        digitalWrite(LEDPins[i],HIGH);
      else
        digitalWrite(LEDPins[i],LOW);
    }
  }
}

void start_sense()  // start/stop button sense
{
  if (millis() - time2 > 250)
  {
    time2=millis();
    isStart = !isStart;
    count=0;
    toggle=true;
    if (isStart)
      Serial.println("Recording Start!!");
    else
      Serial.println("Recording Stop!!");
  }
}

byte bcd2byte (byte data) {
  return 10*(data>>4) + (data & 0x0F);
}

byte byte2bcd (byte data) {
  return (data/10)<<4 | (data%10);
}

// read from DS1307
void regsRead(TimeDate *timeDatePtr) {
  Wire.beginTransmission(i2c_addr);
  Wire.write(0x0);             // register:0
  Wire.endTransmission(false); // false = keep going

  Wire.requestFrom(i2c_addr, 7); // request and read 7 bytes
  
  while (Wire.available()< 7) {  // wait for 7 bytes
  }
  
  timeDatePtr->secs = bcd2byte(Wire.read());
  timeDatePtr->mins = bcd2byte(Wire.read());
  timeDatePtr->hours = bcd2byte(Wire.read());
  timeDatePtr->day = bcd2byte(Wire.read());
  timeDatePtr->date = bcd2byte(Wire.read());
  timeDatePtr->month = bcd2byte(Wire.read());
  timeDatePtr->year = bcd2byte(Wire.read());
}

// write to DS1307
void regsWrite(TimeDate *timeDatePtr) {
  Wire.beginTransmission(i2c_addr);
  Wire.write(0x0);                  // register:0
  Wire.write(byte2bcd(timeDatePtr->secs));
  Wire.write(byte2bcd(timeDatePtr->mins));
  Wire.write(byte2bcd(timeDatePtr->hours));
  Wire.write(byte2bcd(timeDatePtr->day));
  Wire.write(byte2bcd(timeDatePtr->date));
  Wire.write(byte2bcd(timeDatePtr->month));
  Wire.write(byte2bcd(timeDatePtr->year));
  Wire.endTransmission();
}



char * timestamp_get(char * timestampPtr, const TimeDate * timeDatePtr) {
  char temp[10];
  int data;
  *(timestampPtr)='\0';
  
  data = (int)timeDatePtr->year;
  if (data<10)
     strcat(timestampPtr,"0");  
  itoa(data,temp,10);
  strcat(timestampPtr,temp);

  data = (int)timeDatePtr->month;
  if (data<10)
     strcat(timestampPtr,"0");
  itoa(data,temp,10);
  strcat(timestampPtr,temp);

  data = (int)timeDatePtr->date;
  if (data<10)
     strcat(timestampPtr,"0");   
  itoa(data,temp,10);
  strcat(timestampPtr,temp);

  data = (int)timeDatePtr->hours;
  if (data<10)
     strcat(timestampPtr,"0");   
  itoa(data,temp,10);
  strcat(timestampPtr,temp);

  data = (int)timeDatePtr->mins;
  if (data<10)
     strcat(timestampPtr,"0");   
  itoa(data,temp,10);
  strcat(timestampPtr,temp);

  data = (int)timeDatePtr->secs;
  if (data<10)
     strcat(timestampPtr,"0");   
  itoa(data,temp,10);
  strcat(timestampPtr,temp);

  return timestampPtr;
}


void setup() {

  currFreq = 0;
  isStart = false;
  delayTime = delayTable[currFreq];
  count=0;
  
  Serial.begin(9600);
  Wire.begin();
  
  // assign DIO
  pinMode(start_btn, INPUT_PULLUP);
  pinMode(freq_btn, INPUT_PULLUP);
  for (byte i=0; i<freq_no; i++) {
    pinMode(LEDPins[i],OUTPUT);
  }
  
  // attach ISR for two buttons
  attachInterrupt(digitalPinToInterrupt(freq_btn),freq_sense,FALLING);
  attachInterrupt(digitalPinToInterrupt(start_btn),start_sense,FALLING);

  Serial.print("Initializing SD Card...");
  // Check the Card present and can be initialized
  if (!SD.begin(SPI_CS)) {
    Serial.println("Card failed, or not present!");
    // hold with infinite loop
    while(1) {  // flash all LEDs if error.
      for (byte i=0; i<freq_no;i++) {
        if (toggle)
          digitalWrite(LEDPins[i],HIGH);
        else
          digitalWrite(LEDPins[i],LOW);
      }
      toggle=!toggle;  
      delay(500);    
    }
  }
  
  Serial.println("Card Initialized...");
  digitalWrite(LEDPins[currFreq],HIGH);
  toggle=true;
}


File dataFile;
char filename[20];
char timestamp[40];

void loop() {
  // get TimeDate
  regsRead(&currTimeDate);
  timestamp_get(timestamp, &currTimeDate);

  if (count==0 && isStart) {
    strcpy(filename, timestamp+strlen(timestamp)-8);
    strcat(filename,".TXT");
    
    dataFile = SD.open(filename, FILE_WRITE);
    isFileOpen = true;
    dataFile.print("TimeStamp\tADC0\tADC1\tADC2\tADC3\t");
    Serial.println(filename);
  }

  if (isStart) count++;  
  
  // read and record ADC data
  for (byte i=0; i<Max_ADC; i++) {
    if (isStart) {
      if (i==0) {
        dataFile.println();
        dataFile.print(timestamp);
        if (toggle)
          digitalWrite(LEDPins[currFreq],HIGH);
        else
          digitalWrite(LEDPins[currFreq],LOW);
        toggle=!toggle;
      }
      dataFile.print(tab);
      dataFile.print(analogRead(ADCPins[i]));
    }
  }

  if (count>=Max_Records || (isFileOpen && !isStart)) {
    count=0;
    dataFile.close();
  }
  
  delay(delayTime);
}


XXX

 

程式2,

去掉 LED 跟按鈕偵測的程式

 

 

arrow
arrow
    創作者介紹
    創作者 ohiyooo2 的頭像
    ohiyooo2

    早安,苦命工程師的胡言亂語

    ohiyooo2 發表在 痞客邦 留言(0) 人氣()