2.2. Ngơn ngữ lập trình cho vi điều khiển
2.2.2. Những vấn đề với ngơn ngữ C viết cho các bộ vi điều khiển
Ngơn ngữ lập trình C là một ngơn ngữ lập trình đ−ợc sử dụng rộng rãi trên thế giới. Nhiều ng−ời cho rằng đây là ngơn ngữ lập trình bậc cao vì C cĩ khá nhiều thuộc tính giống các ngơn ngữ lập trình bậc cao, ví dụ nh−: cấu trúc của ch−ơng trình, cách định nghĩa và gọi các thủ tục...,tuy nhiên sức mạnh thực sự của C khơng phải ở đĩ mà chính là khả năng truy nhập phần cứng, thao tác trên các bit, byte, word của phần cứng đối t−ợng. Cũng chính bởi lý do này mà C đ−ợc rất nhiều nhà thiết kế hệ điều hành sử dụng là ngơn ngữ lập trình bên cạnh hợp ngữ, cĩ thể kể ra các hệ điều hành cĩ sử dụng C để lập trình nh−: UNIX, LINUX, MS-Windows...
Trên bộ vi điều khiển 8bit, dễ hiểu là khơng thể đem so sánh về tài nguyên đối với các bộ vi xử lý đ−ợc chọn để viết hệ điều hành LINUX hay Windows. Cĩ thể kể ra một số dẫn chứng: tài nguyên về bộ nhớ của vi điều khiển chỉ vài Kbyte đối với bộ nhớ ch−ơng trình và vài trăm byte đối với bộ nhớ dữ liệu; khơng gian địa chỉ cũng chỉ là vài chục Kbyte...
Trình dịch C cĩ khả năng nhận mã nguồn C (C source code) của ng−ời lập trình và tạo ra mã đối t−ợng (object code) đ−ợc tối −u hố cao từ mã nguồn này. Tuy nhiên cĩ nhiều điều mà chúng ta, với t− cách là ng−ời thiết kế, cĩ thể thực hiện để giúp cho trình dịch tạo ra mã tốt hơn để cĩ thể tiết kiệm đợc tối đa các tài nguyên của vốn hạn chế của bộ vi điều khiển.
2.2.2.1. Giảm kích th−ớc biến.
Một trong những điều cơ bản nhất mà ta cần phải làm để cải thiện ch−ơng trình của ta là đặc biệt l−u ý đến kích th−ớc của các biến. Đối với ng−ời th−ờng lập trình bằng C trên các máy chẳng hạn nh− một mainframe hoặc PC, việc khai báo những điều nh− là các bộ đếm vịng lặp d−ới dạng các số nguyên là rất bình th−ờng, ngay cả đối với các biến mà giá trị của chúng khơng bao giờ v−ợt quá 255. Trên một máy 8 – bit nh− 8051, việc sử dụng rộng rãi các kiểu dữ liệu cĩ kích th−ớc lớn hơn 8 bít sẽ gây ra sự hoang phí lớn về khả năng xử lý và bộ nhớ. Ta cần phải cẩn thận xem xét tầm giá trị đối với biến mà ta khai báo và kế đến
chọn kiểu nhỏ nhất thoả yêu cầu. Hiển nhiên kiểu đ−ợc −a chuộng nhất đối với
các biến sẽ là unsigned char, do kiểu này chỉ sử dụng 1 byte.
2.2.2.2. Sử dụng kiểu unsigned.
Đến đây, ta cĩ thể ngạc nhiên vì kiểu đ−ợc −a chuộng là kiểu unsigned
char thay vì là kiểu char. Lập luận đằng sau điều này là 8051 khơng hỗ trợ số học cĩ dấu và đoạn mã phụ đ−ợc yêu cầu bởi một giá trị cĩ dấu, trái ng−ợc với một giá trị khơng dấu, sẽ lấy đi tồn bộ các tài nguyên của bộ vi điều khiển. Nh− vậy cùng với việc chọn lựa các kiểu biến t−ơng ứng với tầm giá trị, ta cũng phải xem xét xem cĩ phải biến sẽ đ−ợc sử dụng cho một thao tác nào đĩ sẽ yêu cầu các số âm. Nếu khơng, ta cần đảm bảo rằng ta chỉ ra biến cĩ kiểu unsigned. Ta cần loại bỏ các số âm ra khỏi một hàm hoặc tồn bộ ứng dụng của ta.
2.2.2.3. Khơng sử dụng số dấu chấm động.
Việc thực hiện các phép tốn số dấu chấm động trên các giá trị 32-bit với một bộ vi điều khiển 8 bit cũng giống nh− ta cắt một bãi cỏ bằng các đồ dùng cắt mĩng tay. Ta cĩ thể thực hiện đ−ợc điều này nh−ng sẽ hoang phí một l−ợng thời gian lớn khủng khiếp. Bất kỳ lúc nào ta dự định sử dụng dụng số dấu chấm động trong một ứng dụng, ta cần phải tự hỏi xem cĩ phải điều này là tuyệt đối cần thiết hay khơng? Thơng th−ờng, các số dấu chấm động cĩ thể đ−ợc loại bỏ bằng cách tăng cấp tất cả các giá trị bằng một cặp cùng bậc về độ lớn và sử dụng các phép tốn số nguyên. Ta sẽ xử lý tốt hơn các số kiểu int và long so với các số kiểu double và float. Ch−ơng trình của ta sẽ thực thi nhanh hơn và các th−ờng trình số dấu chấm động sẽ khơng đ−ợc liên kết vào trong ứng dụng của ta. Ngồi ra nếu ta cần phải sử dụng số dấu chấm động, ta cần xem xét việc sử dụng các phiên bản của 8051, các phiên bản này đã đ−ợc tối −u hố đối với các phép tốn số học chẳng hạn nh− Siemens 80517, 80537 hoặc Dallas Semiconductor 80320.
Cĩ nhiều khi ta bị ép buộc phải liên kết chặt chẽ với việc sử dụng các số dấu chấm động trong hệ thống của ta. Ta đã biết những bất lợi về kích th−ớc ch−ơng trình và tốc độ mà ta phải đối phĩ. Ngồi ra, ta cần ý thức đ−ợc rằng nếu ta sử dụng các phép tốn số dấu chấm động trong một th−ờng trình mà th−ờng trình này cĩ thể bị ngắt, ta phải đảm bảo rằng th−ờng trình ngắt (interrupting routine) khơng đ−ợc sử dụng các phép tốn số dấu chấm động ở bất kỳ nơi nào trong cây gọi (calling tree) của th−ờng trình, hoặc ta phải l−u giữ trạng thái của hệ thống số dấu chấm động ở nơi bắt đầu của ISR bằng cách sử dụng “fpsave” và phục hồi lại trạng thái ban đầu của hệ thống số dấu chấm động ở nơi kết thúc ISR bằng cách sử dụng “fprestore”. Một ph−ơng pháp khác là bao bọc lời gọi đến các ch−ơng trình số dấu chấm động chẳng hạn nh− sin() cùng với một hàm vơ hiệu hố các ngắt tr−ớc lời gọi đến hàm tốn học và cho phép các ngắt trở lại sau lời gọi.
include <math.h>
void timer0_isr(void) interrupt 1 { struct FPBUF fpstate;
// đoạn mã khởi động hoặc đoạn mã
// Khơng sử dụng số dấu chấm động nào đĩ fpsave(&fpstate); // L−u trạng thái của
// hệ thống số dấu chấm động
// đoạn mã ISR bất kỳ, bao gồm // đoạn mã số dấu chấm động.
fprestore (&fpstate); // khơi phục trạng thái của hệ thống
// số dấu chấm động
// đoạn mã ISR bầt kỳ khơng sử dụng // số dấu chấm động
}
float my_sin(float arg) { float retval;
bit old_ea;
old_ea = EA; // l−u trạng thái ngắt hiện hành EA = 0; // cấm các ngắt
Retval = sin(arg); /* thực hiện lời gọi hàm số dấu chấm động*/
EA = old_ea; /* đặt trở lại trạng thái ngắt*/
return retval; }
Nếu ta phải sử dụng số dấu chấm động trong ch−ơng trình của ta, ta cần nỗ lực xác định độ chính xác cực đại cần cĩ. Một khi đã tính đ−ợc số bit cực đại của độ chính xác cần cĩ trong việc tính tốn số dấu chấm động, ta hãy đ−a số này vào trong hộp thoại các lựa chọn của trình dịch bên d−ới điều khiển “Bit to round for float compare”. Điều này sẽ cho phép các th−ờng trình số dấu chấm động giời hạn l−ợng cơng việc mà chúng thực hiện chỉ đến độ chính xác cĩ ý nghĩa đối với hệ thống của ta.
2.2.2.4. Sử dụng biến bit
Khi ta dự định sử dụng các cờ (flag) chỉ cĩ hai trạng thái 0 và 1, ta nên sử dụng kiểu bit thay vì sử dụng kiểu unsigned char. Điều này nhằm mục đích để dành bộ nhớ cho sau này vì ta khơng hoang phí 7 bit kia. Ngồi ra, các biến bit luơn luơn đ−ợc cất ở RAM bên trong và do vậy sẽ đ−ợc truy cập trong một chu kỳ.
3.8.5. Sử dụng biến cục bộ thay cho biến tồn cục
Các biến đ−ợc khai báo là dữ liệu tồn cục (global data) sẽ sử dụng kém hiệu quả hơn so với các biến đ−ợc khai báo là dữ liệu cục bộ (local data). Lý do cho điều này là trình dịch ln cố gắng gán các biến cục bộ cho các thanh ghi bên trong, trong khi đĩ dữ liệu tồn cục cĩ thể hoặc khơng thể ở trong các thanh ghi bên trong tuỳ thuộc và khai báo của ta. Trong những tr−ờng hợp mà các biến tồn cục đ−ợc gán mặc định cho XDATA (chẳng hạn nh− các ch−ơng trình theo mơ hình bộ nhớ lớn), ta đã từ bỏ việc truy cập nhanh. Lý do khác để tránh các biến tồn cục là ta phải phối hợp việc truy cập đến các biến nh− vậy giữa các quá trình trong hệ thống của ta. Điều này sẽ là một vấn đề trong các hệ thống ngắt hoặc các hệ thống đa tác vụ, trong đĩ cĩ thể cĩ nhiều hơn một quá trình sẽ cố gắng sử dụng biến tồn cục. Nếu ta cĩ thể tránh các biến tồn cục và sử dụng các biến cục bộ, trình dịch sẽ quản lý ch−ơng trình của ta một cách cĩ hiệu quả nhất.
2.2.2.6. Sử dụng bộ nhớ bên trong cho các biến.
Các biến tồn cục và cục bộ cĩ thể bị buộc ở trong một vùng nhớ bất kỳ mà ta muốn. Nh− ta đã biết qua các thảo luận ở trên về các thoả hiệp giữa các đoạn nhớ, ta cần thấy rõ rằng ta cĩ thể tối −u hố tốc độ của ch−ơng trình bằng cách đặt các biến đ−ợc sử dụng th−ờng xuyên ở RAM bên trong. Ngồi ra ch−ơng trình của ta sẽ nhỏ hơn do cĩ ít lệnh hơn và cung cấp việc truy cập các biến ở RAM bên trong nhanh hơn so với ch−ơng trình phải truy cập các biến ở RAm ngồi. Nhằm tăng tốc độ thực thi ch−ơng trình, ta sẽ muốn lấp đầy các đoạn nhớ theo thứ tự sau: DATA, IDATA, PDATA, XDATA. Một lần nữa ta cần cẩn thận để dành đủ khơng gian nhớ trong đoạn nhớ DATA cho vùng xếp chồng bên trong của bộ vi điều khiển.
2.2.2.7. Sử dụng các con trỏ bộ nhớ cụ thể.
Nếu ch−ơng trình của ta dự định sử dụng các con trỏ cho các thao tác nào đĩ, ta cĩ thể muốn khảo sát cách sử dụng chúng và l−u giữ chúng trong một vùng nhớ cụ thể chẳng hạn nh− khơng gian XDATA hoặc CODE. Nếu ta cĩ thể thực hiện điều này, ta sẽ sử dụng các con trỏ bộ nhớ cụ thể. Nh− đã đề cập tr−ớc đây, các con trỏ bộ nhớ cụ thể sẽ khơng cần đến một bộ chọn và ch−ơng trình sử dụng chúng sẽ chặt chẽ hơn do trình dịch cĩ thể bổ sung chúng để áp dụng một tham chiếu đến một đoạn nhớ cho tr−ớc thay vì phải xác định đoạn nhớ nào con trỏ nhắm đến.
2.2.2.8. Sử dụng hàm nội tại
Đối với những hàm đơn giản chẳng hạn nh− quay bit các biến (bit – wise rotation of variables), trình dịch cung cấp cho ta các hàm nội tại (intrinsic function), các hàm này cĩ thể đ−ợc gọi. Nhiều hàm nội tại t−ơng ứng trực tiếp với các lệnh của hợp ngữ trong khi những hàm khác đ−ợc yêu cầu nhiều hơn và cung cấp tính t−ơng thích của ANISI. Tất cả các hàm nội tại là hàm reentrant và do vậy cĩ thể đ−ợc gọi một cách an tồn từ bất kỳ nơi nào trong ch−ơng trình.
Đơi với các thao tác quay trên một byte, các hàm nội tại _crol_ (quay
trái) _cror_ (quay phải) t−ơng ứng trực tiếp với các lệnh hợp ngữ ‘RL A’ và
‘RR A’. Nếu ta muốn thực hiện phép quay bít trên các đối t−ợng lớn hơn chẳng hạn nh− một đối t−ợng cĩ kiểu int hoặc long, hàm nội tại sẽ phức tạp hơn và do vậy sẽ dài hơn. Các phép quay này cĩ thể đ−ợc gọi bằng cách sử dụng các hàm nội tại _irol, _iror_ cho các kiểu int và các hàm nọi tại _lrol,
_lror_ cho các kiểu long.
Lệnh “nhảy và xố bít nếu bít đ−ợc thiết lập bằng 1” (“jump and clear bit if set”) JBC từ cũng đ−ợc thực hiện d−ới dạng một hàm nội tại để sử dụng trong C. Hàm này đ−ợc gọi bởi _testbit_. Hàm nội tại này trả về giá trị đúng nếu bít thơng số đ−ợc thiết lập bằng 1 và sai nếu ng−ợc lại. Điều này th−ờng hữu dụng khi kiểm tra các cờ chẳng hạn nh− RI, TI hoặc các cờ tràn của bộ định thời và dẫn đến ch−ơng trình viết bằng C dễ đọc hơn nhiều. Hàm này cĩ thể dịch trực tiếp thành lệnh JBC. Ví dụ sau đây sẽ minh hoạ rõ hơn điều này
#include<instrins.h>
void serial_intr(void) interrupt 4 {
if (!_testbit_(TI)){ /* nếu đây là ngắt truyền*/ P0 = 1; // đổi trạng thái P0.0. _nop_(); // chờ 1 chu kỳ P0 = 0; .... /* thực hiện các câu lệnh khác...*/ } if (!_testbit_(RI)){ test = _cror_(SBUF,1); ..../* thực hiện các thao tác khác*/. } }
Xác yêu cầu thiết kế
Lựa chọn cấu hình cho hệ thống theo yêu cầu thiết kế
Thiết kế mạch điện và lắp ráp linh kiện
Đo thử và kiểm tra lỗi
Ch−ơng 3 - thiết kế hệ vi điều khiển
Thiết kế phần cứng là thiết kế mạch điện tử bao gồm các linh kiện, phụ kiện phù hợp tạo thành một hệ vi điềukhiển hợp nhất. Nĩi chung một hệ vi điều khiển cũng t−ơng đ−ơng với một hệ máy tính, tức là nĩ phải cĩ đủ 03 khối chính: Khối xử lý trung tâm, khối bộ nhớ và khối vào ra. Chi tiết về từng khối này nh− thế nào là tuỳ thuộc vào từng ứng dụng cụ thể. Cĩ những ứng dụng yêu cầu xử lý tính tốn nhanh thì cần một bộ xử lý mạnh, cĩ ứng dụng yêu cầu l−u trữ lớn thì cần phải cĩ bộ nhớ cĩ dung l−ợng lớn...Khối cần phải tuỳ biến nhiều nhất chính là khối vào ra bởi lẽ khối này chính là khối giao tiếp với thế giới thực để tạo ra các ứng dụng thực. Nĩi chung để thiết kế đ−ợc một hệ vi điều khiển cĩ khả năng hoạt động tốt, độ tin cậy cao thì ng−ời thiết kế cần phải đi theo một trình tự nhất định, cũng cĩ thể gọi là nguyên tắc thiết kế. Cĩ 2 nhĩm ngun tắc chính đĩ là đi từ trên xuống và đi từ d−ới lên. Nguyên tắc đi từ trên xuống th−ờng đ−ợc áp dụng với các hệ nhúng nhỏ, cụ thể trong tr−ờng hợp thiết kế một hệ vi điều khiển thì trình tự thiết kế nh− sau: