製作隔空感應的引擎轉速表 - 新唐N76S003計時器輸入捕捉的應用(Timer input capture)

先前說過,用新唐1T 8051 N76S003製作的003Clock套件不只是時鐘而已,設計時將所有IO接腳都拉出來了,因此它還能夠做其它有趣的應用!

這次分享一個應用實例是「隔空感應的引擎轉速表」。老樣子,會在留言區和GitHub提供完整程式碼。

https://github.com/danchouzhou/003Clock/blob/f8fa5d909b5bdadf008ea9de9164a25ca33d9015/firmware/Tachometer/main.c

引擎轉速信號

轉速表的基本原理都是測量點火之間的週期或頻率,使用類比電路、數位電路或MCU中的程式換算出轉速。那麼點火和引擎轉速有什麼關聯呢?我們看一下維基百科可以得知,當今主流的四行程引擎運作時,720度中分為 進氣、壓縮、爆炸、排氣 共四個行程,其中爆炸行程就是靠與高壓線圈相連接的火星塞產生的火花,點燃在燃燒室中的壓縮混合氣,讓氣體溫度上升,膨脹時讓引擎產生動力。而高壓線圈可以在收到點火信號時,輸出數十KV讓火星塞產生火花。所以可以得知,點火與引擎轉速呈現線性關係,經過換算後即可得知引擎轉速。取得轉速信號最直接的方式當然就是把高壓線圈的點火信號拉出來用。

高壓線圈產生的EMI可以當成是轉速信號?

一般來說,雜訊在電路裡是我們不樂見的干擾源,但我卻將高壓線圈產生的電磁波當成是轉速信號。下圖是發動之後,將示波器探棒靠近引擎量到的波形,信號可是非常清晰的,注意喔,在這裡探棒並未直接與車輛電路相連,是隔空就能測得到信號。經過實際測試,將微控制器的IO用20公分左右的線接出來,並設定成pull-up模式(這可以避免輕微的雜訊誤觸發),接條線出來當天線靠近引擎,IO準位也會隨之跳動,完全不用額外的電路或感測器。如此以來不用改裝原車線路,就能夠「隔空」得到轉速信號。

在這邊將示波器時基設為50 ms/DIV、探棒設為1:1、垂直設為10 V/DIV,trigger level設成-20 V,可以看見trigger頻率是13.8 Hz,大約就是摩托車引擎怠速1600 RPM點火的頻率(後面會再講解頻率如何換算成轉速)。


如何得到兩次點火的間隔?

還記得之前介紹過的計時器吧!要得到兩次點火的間隔,可以開一個Timer並將IO設定成邊緣觸發的外部中斷,每次中斷發生時把Timer計數值讀取出來,再把Timer清除重新開始計時。已知Timer的時脈頻率,就可以從計數值計算出兩次中斷的間隔時間。但是會有誤差:中斷延遲(interrupt latency)以及中斷服務程式執行的時間,都會是產生誤差的原因(之前提過編譯器能保證程式邏輯正確,但不能保證執行時間、效率)。

Timer的輸入捕捉模式

前段敘述的步驟要是能靠硬體完成,就能避免軟體產生的誤差了。沒錯!那就是輸入捕捉(Input Capture)。

除了標準8051就有的Timer 0和1之外,新唐N76S003額外提供Timer 2和3,這讓計時、計數方面的應用能有更大的彈性。其中Timer 2還有輸入捕捉的功能。從它的方塊圖可以看到,下半部分是Timer 2本身,一個標準的16-bit Timer;而上半部分則是輸入捕捉模組(Input Capture Module),主要由邊緣檢測電路和栓鎖器組成,當邊緣觸發發生時,硬體會將Timer 2當下的計數值儲存進暫存器C0H, C0L,同時將CAPF0(Input Capture 0 Flag)旗標設為’1’供中斷、軟體判斷使用;當CAPCR(Capture Auto-Clear)為’1’時,CAPF0還能當成Timer 2的清除信號。這樣的組合,是不是就能達到前段敘述的功能呢?

Timer 2設定成輸入捕捉模式

將Timer 2設定成捕捉模式時要留意以下的設定

  1. Prescaler:決定Timer 2的計時速度,評估待測信號的頻率範圍,在最低頻率時不能讓Timer算到溢位且最高頻率要低於Timer輸入的時脈頻率
  2. 觸發斜率:決定邊緣檢測電路是正緣觸發、負緣觸發或正負緣都觸發
  3. 捕捉模組與輸入腳位:N76S003有CAP0~CAP2共3個捕捉模組,每個捕捉模組都可以從9個IO中擇一當成輸入捕捉的信號來源,能當輸入捕捉的IO詳見N76S003的手冊
  4. Auto-Clear或Auto-Reload:從T2MOD暫存器中的CAPCR和LDTS可以設定,捕捉到信號時要自動將Timer 2清除,或是RCMP2H, RCMP2L載入初值。前者適合計算兩個信號的間隔時間,後者適合自動判斷兩個信號是否超過設定的時長(還有很多應用方式請讀者發揮想像力)

在這次轉速表的應用中,將偵測轉速範圍設計在500 RPM ~ 10000 RPM,以四行程引擎來說換算成頻率就是4.16 Hz ~ 83.33 Hz。因此prescaler要設定成1/64才能確保在最低轉速Timer還不會溢位。計算方式如下:

16000000 Hz / 64 / 4.16 Hz = 60096.15

若prescaler降一階為1/32時,會超過Timer 2 16-bit 0 ~ 65535的計數範圍,因此1/64會是最佳選擇:

16000000 Hz / 32 / 4.16 Hz = 120192.3

要將Timer 2設定為捕捉模式很簡單,只有下面三行程式碼。在T2MOD將Timer 2的prescaler設為1/64並將Capture Auto-Clear致能,接著從CAPCON0把Input Capture Module 0打開,最後設定T2CON讓Timer 2以Auto-Reload Mode開始計時。另外CAPCON1CAPCON3分別可以設定邊緣觸發斜率與捕捉的IO腳,我們用開機預設的負緣觸發、P1.2作為捕捉信號來源,就不再特別設定它們了。

  1. T2MOD = 0x48; // Timer 2 capture auto-clear, pre-scalar = 1/64
  2. CAPCON0 |= 0x10; // Input Capture 0 Enable
  3. // CAPCON1 &= 0xFC; // Input Capture 0 Level Select = Falling edge
  4. // CAPCON3 &= 0xF0; // Input Capture Channel 0 Input Pin = P1.2/IC0
  5. T2CON = 0x04; // Timer 2 run

將捕捉數值換算為頻率、引擎轉速

首先我們要先將捕捉到的數值C0H, C0L合併成16-bit的資料:

  1. u16cap0 = (uint16_t)C0H << 8 | C0L; // Get 16-bit capture data

可以這樣將捕捉數值換算為頻率:

Timer 2的輸入頻率 / 捕捉數值

由於我們有設定prescaler 1/64,Timer 2的輸入頻率會是Fsys 16 MHz / 64 = 250 KHz,所以這邊要帶入250000。

  1. u16freq = 250000 / u16cap0;

得到的頻率其實就已經是每秒轉速了,可以由頻率乘上60即可換算為我們常用的單位RPM(Rev Per Minute, 每分鐘轉幾圈),不過四行程引擎轉兩圈只會點火一次,所以還要再乘2。

頻率 x 60 x 2 = 引擎轉速

但是如果直接由剛剛算出來的u16freq直接乘120,解析度會變很差,原因是因為整數除法不會保留小數點。所以我們應該在原先換算頻率的公式,將它的被除數乘120倍,直接拿原始資料u16cap0來當除數。

  1. u16rev4stroke = 30000000 / u16cap0;

另外就是比較舊的一點的車輛是C.D.I.點火或白金點火的化油器四行程引擎,有別於EFI引擎,ECU能夠分辨壓縮或排氣行程,C.D.I.和白金點火如同二行程一樣每轉一圈360度都會點火一次,點火頻率會是四行程EFI引擎的兩倍,所以還是要把相容模式做進去,然後透過003Clock上面的按鈕開關切換模式。

  1. u16rev2stroke = 15000000 / u16cap0;

成果實測

003Clock的排針孔位可以免焊接直接插上排針,我們將P1.2連接一條20公分的杜邦線當作天線,然後靠近摩托車引擎。

實測影片是測量摩托車引擎的怠速轉速(ECU設定的怠速目標為1600轉),要一手拿著003Clock,一手拿著攝影機,就不方便加油門改變引擎轉速了。

完整程式碼:
  1. #include "numicro_8051.h"
  2. #include "PT6961.h"
  3.  
  4. #define SW1 P17
  5.  
  6. void main(void)
  7. {
  8. uint16_t u16counter = 0;
  9. uint16_t u16cap0 = 0;
  10. uint16_t u16freq = 0, u16rev4stroke = 0, u16rev2stroke = 0;
  11. uint16_t u16swCount = 0;
  12. uint8_t u8mode = 0;
  13.  
  14. /* Switch to external clock source */
  15. TA = 0xAA;
  16. TA = 0x55;
  17. CKEN |= 0xC0; // EXTEN
  18. while(!(CKSWT & 0x08)); // ECLKST
  19. TA = 0xAA;
  20. TA = 0x55;
  21. CKSWT |= 0x02; // Switch to external clock source
  22. while(CKEN & 0x01); // CKSWTF
  23. TA = 0xAA;
  24. TA = 0x55;
  25. CKEN &= 0xDF; // Disable HIRC
  26.  
  27. P17_QUASI_MODE;
  28. P12_QUASI_MODE;
  29.  
  30. pt6961_init();
  31.  
  32. CKCON |= 0x08; // Timer 0 source from Fsys directly
  33. TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000
  34. TL0 = (uint8_t)(49536 & 0xFF);
  35. TMOD |= 0x01; // Timer 0 mode 1
  36. TCON |= 0x10; // Timer 0 run
  37.  
  38. T2MOD = 0x48; // Timer 2 capture auto-clear, pre-scalar = 1/64
  39. CAPCON0 |= 0x10; // Input Capture 0 Enable
  40. // CAPCON1 &= 0xFC; // Input Capture 0 Level Select = Falling edge
  41. // CAPCON3 &= 0xF0; // Input Capture Channel 0 Input Pin = P1.2/IC0
  42. T2CON = 0x04; // Timer 2 run
  43.  
  44. while(1)
  45. {
  46. if (SW1) {
  47. if (u16swCount > 20)
  48. u8mode++;
  49. u16swCount = 0;
  50. }
  51. else
  52. u16swCount++;
  53.  
  54. if (CAPCON0 & 0x01) // Check capture 0 flag
  55. {
  56. CAPCON0 &= 0xFE; // Clear capture 0 flag
  57. u16cap0 = (uint16_t)C0H << 8 | C0L; // Get 16-bit capture data
  58. if (u16cap0 != 0)
  59. {
  60. u16freq = 250000 / u16cap0; // 16 MHz / 64 = 250000, the minimum capture frequency is 250000 Hz / 65536 = 3.81 Hz
  61. u16rev4stroke = 30000000 / u16cap0; // frequency * 60 * 2 = 4-stroke rev
  62. u16rev2stroke = 15000000 / u16cap0; // frequency * 60 = 2-stroke rev
  63. }
  64.  
  65. if (u16counter > 100) // Minimum update interval is 100 ms
  66. {
  67. switch (u8mode) {
  68. case 0:
  69. if (u16rev4stroke < 10000) { // Check the data within the display range
  70. pt6961_setNumber(u16rev4stroke, 0);
  71. u16counter = 0;
  72. }
  73. break;
  74.  
  75. case 1:
  76. if (u16rev2stroke < 10000) {
  77. pt6961_setNumber(u16rev2stroke, 0);
  78. u16counter = 0;
  79. }
  80. break;
  81.  
  82. case 2:
  83. if (u16freq < 10000) {
  84. pt6961_setNumber(u16freq, 0);
  85. u16counter = 0;
  86. }
  87. break;
  88.  
  89. default:
  90. u8mode = 0;
  91. }
  92. }
  93. }
  94.  
  95. if (u16counter > 2000) // Clear display if no valid data for 2 seconds
  96. {
  97. pt6961_writeCommand(0xC0);
  98. pt6961_writeByte(0x40); // 'g' segment
  99. pt6961_writeCommand(0xC2);
  100. pt6961_writeByte(0x40);
  101. pt6961_writeCommand(0xC4);
  102. pt6961_writeByte(0x40);
  103. pt6961_writeCommand(0xC6);
  104. pt6961_writeByte(0x40);
  105. }
  106. else
  107. u16counter++;
  108. /* 1 ms delay */
  109. while (!(TCON & 0x20)); // Wait until timer 0 overflow
  110. TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000
  111. TL0 = (uint8_t)(49536 & 0xFF);
  112. TCON &= 0xDF; // Clear TF0
  113. }
  114. }

留言

這個網誌中的熱門文章

無法被取代的指針型三用電表(一):前言

關於新唐科技NuMicro ISP的介紹和使用方式

新唐火神板開箱實作(一):NuMaker-Volcano與NuEclipse IDE入門篇