Thứ Năm, 6 tháng 1, 2022

4: Bộ Nhớ Và Hiến Thị Kí Tự (Phần 1) - Khái Niệm Chung, Các Toán Tử Bitwise, Bộ Nhớ Màn Hình

 

4: Bộ Nhớ Và Hiến Thị Kí Tự


- Khái Niệm Chung

Trong phần này ta sẽ xem xét việc xử lí hiển thị kí tự bằng cách xâm nhập trực tiếp vào bộ nhớ (direc memory access-DMA).
Ta sẽ tìm hiểu cách xâm nhập trực tiếp màn hình. Cách này nhanh hơn là dùng các hàm của C.

- Các Toán Tử Bitwise

1. Toán tử bitwise and (&):
C dùng 6 toán tử bitwise được tóm tắt trong bảng sau

Phép toán Kí hiệu
+ and &
+ OR |
+ XOR ˆ
+ dịch phải >>
+ dịch trái <<
+ đảo ~

Các phép toán này có thể áp dụng cho dữ liệu kiểu int, char nhưng không áp dụng cho số float.
Toán tử & (khác với and logic &&) cần hai toán hạng có kiểu giống nhau . Các toán hạng này được and bít với bit. Toán tử & thường dùng kiểm tra xem một bit cụ thể nào đó có trị là bao nhiêu.

Ví dụ để kiểm tra bit thứ 3 cuả biến ch có trị 1 hay O ta dùng phép toán :
ch &0x08;

2. Toán tử or:
Toán tử or (khác or logic | l) thường dùng kết hợp các bịt từ các biến khác nhau vào một biến duy nhất.
Ví dụ ta có hai biến là ch1 và ch2 và giả sử các bịt từ 0..3 của ch1 chứa các trị mong muốn còn các bit 4..7 của ch2 chứa các trị mong muốn. Khi viết:
a = ch1|ch2;

thì cả 8 bit của a đều chứa trị mong muốn.

3. Toán tử dịch phải >>:
Toán tử này làm việc trên một biến duy nhất. Toán tử này dịch từng bit trong toán hạng sang phải. Số bit dịch chuyển được chỉ định trong số đi theo sau toán tử.
Việc dịch phải một bit đồng nghĩa với việc chia toán hạng cho 2.

Vídụ: 01110010 dịch sang phải 1 bit sẽ là
0 0 1 1 1 0 0 1

4. Đổi từ số hex sang số nhị phân:
Ta dùng các toán tử bitwise để đổi một số từ hệ hex sang hệ 2.
Chương trình như sau:
Chương trình 4-1 :
// Memory_1
#include <conio.h>
#include <stdio.h>

int main() {
  int i, num, bit;
  unsigned int mask;
  char string[10], ch;


  do {
    mask = 0x8000;
    printf("\nBan cho mot so : ");
    scanf("%x", & num);
    printf("Dang nhi phan cua so %x la : ", num);
    for (i = 0; i < 16; i++) {
      bit = (mask & num) ? 1 : 0;
      printf("%d", bit);
      if (i == 7) printf("  ");
      mask >>= 1;
    }
    printf("\nBan muon tinh tiep khong(c/k)?");
    ch = getch();
  } while (ch == 'c');
  getch();
  
  return 0;
}

Trong chương trình trên ta dùng vòng lặp for để duyệt qua 16 bit của biến nguyên từ trái qua phải. Lõi của vấn đề là các phát biểu:
bit = (mask&num)? 1 : 0;  
mask >>=1

Trong phát biểu đầu tiên mask là biến chỉ có một bit 1 duy nhất ở phía trái nhất.
Mask này được & với num để xem bít trái nhất của num có là 1 hay là O. Nếu kết quả khác O (true) bit tương ứng của num là 1 còn ngược lại bit tương ứng là O.

Sau mỗi lần & mask được dịch trái 1 bit để xác định bit tiếp theo của num là 0 hay 1.

5. Các toán tử bitwise khác:
a. Toán tứ xor ^:
+ Toán tử xor trả về trị 1 khi chỉ có 1 bit chứ không phải 2 bít có trị là 1
+ Toán tử xor cần khi lật bit nghĩa là hoán chuyển giữa 1 và O vì 1 xor với 1 là 0 và 1 xor với 0 là 1.

Ví dụ để lật bit thứ 3 trong biến ch ta dùng:
ch ^ 0x08

b. Toán tử dịch phổi <<:
Toán tử này tương tự toán tử dịch trái . Giá trị của bit chèn vào bên phải luôn luôn bằng 0O. Dịchphải tương ứng với việc nhân số đó cho 2.

c. Toán tử đảo:
Toán tử này là toán tử một ngôi. Nó tác động lên các bit của toán hạng và đảo trị của bit từ 1 sang 0 và từ 0 sang 1. Đảo 2 lần một số ta lại nhận được số cũ.

- Bộ Nhớ Màn Hình

1. Khái niệm chung:
Màn hình thông thường có 25 dòng và mỗi dòng có 80 kí tự.
Như vậy toàn bộ màn hình có 2000 kí tự . Mỗi kí tự trên màn hình tương ứng với một địa chỉ cụ thể trong bộ nhớ màn hình.
Mỗi kí tự dùng 2 byte của bộ nhớ này: byte thứ nhất chứa mã ASCII của kí tự (từ 0 đến 255 nay từ 0O đến ff)và byte thứ 2 chứa thuộc tính của nó.

Như vậy để hiển thị 2000 kí tự, bộ nhớ màn hình phải cần 4000 byte.
Bộ nhớ màn hình đơn sắc bắt đầu tại B000h và kết thúc tại B0F9F.
Nếu ta có màn hình mầu thì vùng nhớ cho chế độ văn bản sẽ bắt đầu từ B8000h và kết thúc tại B8F9F.

Khi muốn hiển thị kí tự trên màn hình, đoản trình thư vện C sẽ gọi đoản trình ROM-BIOS để đặt kí tự cần hiển thị vào địa chỉ tương ứng trong bộ nhớ nàm hình. Nếu muốn có tốc độ nhanh, hãy chèn trực tiếp các giá trị trên vào vùng nhớ màn hình.

2. Con trỏ far:
Khi biết địa chỉ, cách thông dụng để đưa giá trị vào bộ nhớ là dùng con trỏ.
Do vậy nếu ta cần đưa kí tự vào vị trí đầu tiên của vùng nhớ màn hình thì ta viết:
int *ptr ;  
ptr = 0xB800;
*(ptr)=ch;

Đoạn chương trình trên có vẻ hợp lí nhưng lại không làm việc vì biến con trỏ thông thường có hai byte trong khi địa chỉ B0000h lại dài 5 chữ số (2,5 byte).

Lý do dẫn đến tình trạng này là do con trỏ thường dùng để chứa đại chỉ nằm trong một đoạn duy nhất mà thôi.
Trong họ 8086, một đoạn dài 10000h hay 65535 byte. Bên trong các đoạn địa chạy từ 0h đến FFFEh.

Thông thường các đữ liệu của chương trình C nằm trong một đoạn nên để thâm
nhập các địa chỉ nằm ngoài đoạn ta phải dùng một cơ chế khác. Bên trong 8086, tình trạng này được khắc phục bằng cách dùng các thanh ghi gọi là thanh ghi đoạn.

Các địa chỉ nằm ngoài đoạn đưcợ tạo lập bằng tổ hợp địa chỉ đoạn và địa chỉ offset.
B 0 0 0
  0 7 D 0
----------
B 0 7 D 0

Trong hình trên địa chỉ đoạn BOOOh được dịch trái 4 bịt rồi cộng với địa chỉ offset 07D0 tạo ra địa chỉ tuyệt đối B07D0h.

3. Dùng địa chỉ đoạn offset trong C:
Như vậy khi địa chỉ nằm bên ngoài đoạn dữ liệu, C dùng tổ hợp đoạn:
offset và yêu cầu dạng biểu diễn 32 bit(4 byte, 8 chữ số hex) với 4 chữ số cho địa chỉ đoạn và 4 chữ số cho địa chỉ offset.
Do vậy C coi địa chỉ tuyệt đối B07D0 là 0xB00007D0 (B000 và theo sau là 07D0).

Trong C con trỏ 32 được tính bằng cách dịch địa chỉ đoạn sang trái 16 bit và cộng với địa chỉ offset. Do con trỏ thông thường không thể cất giữ địa chỉ dài 32 bit nên ta phải dùng con trỏ far Con trỏ này cất giữ địa chỉ dài 4 byte.
Vì vậy chương trình sẽ là:
int far *ptr ;  
ptr = 0xB8000000;
*(ptr)=ch;

4. Dùng một kí tự để tô màn hình:
Chúng ta dùng con trỏ far để ghi lên màn hình 2000 bản sao của một kí tự.
Điều này tương tự như dùng putch(). Chương trình kết thúc ghi gõ x
Chương trình 4-2 :
// Memory_2
#include <conio.h>
#include <stdio.h>
#define length 2000

int main() {     
    int far *fptr;     
    int add;     
    char ch;     
    printf("Go vao mot ki tu , go lai de thay doi");     
    fptr=(int far*)0xB8000000;     
    while((ch=getche())!='x')       
        for (add=0;add<length;add++)  
            *(fptr+add)=ch|0x0700;   
    
    return 0;
}

Trong chương trình phát biểu :
*(fptr+add)=ch|0x0700

dùng để điển đầy kí tự lên màn hình. Biến ch là kí tự muốn đặt vào bộ nhớ. Hằng số 0x0700 là byte thuộc tính, có nghĩa là không chớp nháy, không đậm, chữ trắng trên nền đen.

Phát biểu khác cần giải thích :
fptr=(int far*)0xB8000000;

Ta dùng dấu ngoặc vì hằng 0xB8000000 và biến int far fptr có kiểu khác nhau: hằng có vẻ là số nguyên dài còn fptr lại là con trỏ chỉ đến kiểu int.

Do đó để tránh nhắc nhở của trình biên dịch ta cần biến đổi kiểu làm hằng trở tthành con trỏ far chỉ đến int. Màn hình có thể được xem như một mảng hai chiều gồm các hàng và cột . Địa chỉ tương ứng trong bộ nhớ được tính từ phép nhân số hiệu hàng với số lượng cột trong một hàng rồi cộng kết quả và số hiệu cột với địa chỉ bắt đầu của vùng nhớ màn hình.
Ta có chương trình sau:
Chương trình 4-3 :
// Memory_3
#include <conio.h>
#include <stdio.h>
#define rowmax 25
#define colmax 80

void main() {
    int far * fptr;
    int row, col;
    char ch;


    printf("Go vao mot ki tu , go lai de thay doi");
    fptr = (int far * ) 0xB8000000;
    while ((ch = getche()) != 'x')
        for (row = 0; row < rowmax; row++)
            for (col = 0; col < colmax; col++) * (fptr + row * colmax + col) = ch | 0x0700;
            
    return 0;
}

5. Trình xử lí văn bản theo dòng:
Để biết thên về sự tiện lợi của con trỏ far ta xét thêm một trình xử lí văn bản theo dòng. Trình xử lí này chỉ làm việc trên một dòng văn bản.

Ta sẽ tiến hành theo 2 bước: đầu tiên là một chương trình cho phép người dùng gõ vào một dòng và đi chuyển con nháy tới lui.

Có thể xoá kí tự nhờ di chuyển con nháy tới đó và ghi đè lên
nó.
Chương trình như sau:
Chương trình 4-4 :
// Memory_4
#include <conio.h>
#include <dos.h>
#define colmax 80
#define rarrow 77
#define larrow 75
#define video 0x10
#define ctrlc '\x03'

int col=0;
int far *fptr;
union REGS reg;

void cursor() {
    reg.h.ah = 2;
    reg.h.dl = col;
    reg.h.dh = 0;
    reg.h.bh = 0;
    int86(video, & reg, & reg);
}

void insert(char ch) {
    *(fptr + col) = ch | 0x0700;
    ++col;
}

void clear() {
    int j;
    for (j = 0; j < 2000; j++)
        * (fptr + j) = 0x0700;
}


int main() {
    char ch;
    void clear(void);
    void cursor(void);
    void insert(char);
    fptr = (int far * ) 0xB8000000;
    clear();
    cursor();
    while ((ch = getch()) != ctrlc) {
        if (ch == 0) {
            ch = getch();
            switch (ch) {
            case rarrow:
                if (col < colmax) ++col;
                break;
            case larrow:
                if (col > 0) --col;
                break;
            }
        } else if (col < colmax) insert(ch);
        cursor();
    }
    
    return 0;
}

Để xoá màn hình ta điền số O vào vùng nhớ màn hình với thuộc tính Ø7 . Sau đó con nháy được di chuyển về đầu màn hình nhờ phục vụ ấn định vị trí con nháy như sau: ngắt 10h

ah=O;
dh=số hiệu dòng
dl= số hiệu cột
bh=số hiệu trang, thường là O

Phát biểu switch dùng để đoán nhận các phím được nhận là phím thường hay phím chức năng. Phím mũi tên dùng tăng giảm col và gọi hàm cursor() để di chuyển con nháy tới đó. Nếu kí tự gõ vào là kí tự thường, nó được chèn vào nhờ hàm insert().

6. Byfe thuộc tính:
Một kí tự trên màn hình được lưu giữ bởi 2 byte: một byte là mã của kí tự và byte kia là thuộc tính của nó.

Byte thuộc tính được chia làm nhiều phần, bit nào bằng 1 thì thì thuộc tính tương ứng được bật.
Bit thứ 3 điều khiển độ sáng còn bit thứ 7 điểu khiển độ chớp nháy.
Các bit còn lại là:
6 - thành phần đỏ của màu nền;
5 - thành phần green của màu nền;
4 - thành phần blue của mầu nền;
2 - thành phần đỏ của màu chữ ;
1 - thành phần green của mầu chữ;
0 - thành phần blue của màu chữ.

Ta lập một chương trình để điển đầy màn hình bằng các kí tự chớp nháy.
Chương trình 4-5 :
// Memory_5
#include <conio.h>
#include <stdio.h>
#define rowmax 25
#define colmax 80

int main() {
    int far * fptr;
    int row, col;
    char ch;


    printf("Go vao mot ki tu , go lai de thay doi");
    fptr = (int far * ) 0xB8000000;
    while ((ch = getche()) != 'x')
        for (row = 0; row < rowmax; row++)
    for (col = 0; col < colmax; col++)
        * (fptr + row * colmax + col) = ch | 0x8700;


    return 0;
}

Để bật chớp nháy ta để bít thứ 7 thành 1, 3 bit màu nền 0, 1 và 2 được đặt trị 1 nên nền sẽ là đen. Byte thuộc tính lúc này là 10000111 = 87h.

7. Chương trình điền thuộc tính:
Để hiểu sâu hơn thuộc tính của kí tự ta xét chương trình sau
Chương trình 4-6 :
// Memory_6
#include <conio.h>
#include <stdio.h>
#define rowmax 25
#define colmax 80

void fill(char ch,char attr)   {     
    int far *fptr;     
    int row,col;     
    fptr=(int far*)0xB8000000;     
    for (row=0;row<rowmax;row++)
        for (col = 0; col < colmax; col++)
            * (fptr + row * colmax + col) = ch | attr << 8;
}


int main()   {     
    int far *fptr;     
    char ch,attr=0x07;     
    void fill(char,char);     
  
    printf("Go n cho chu binh thuong,\n");    
    printf("Go b cho chu xanh nuoc bien,\n");     
    printf("Go i cho chu sang,\n");     
    printf("Go c cho chu chop nhay,\n");     
    printf("Go r cho chu dao mau\n");     
    while((ch=getche())!='x')   {  
        switch (ch)   {      
            case 'n':
                attr=0x07;      
                break;      
            case 'b':
                attr=attr&0x88;      
                attr=attr|0x01;      
                break;      
            case 'i':
                attr=attr^0x08;      
                break;      
            case 'c':
                attr=attr^0x80;      
                break;      
            case 'r':
                attr=attr&0x88;      
                attr=attr|0x70;      
                break;    
        }  
        fill(ch,attr);       
        
    }   
    
    return 0;
}

Trong hàm fillQ ta có lệnh
*(fptr+row*colmax+col)=ch|attr<<8;

vì attr là kí tự nên phải dịch trái 8 bit trước khi kết hợp với ch.

8. Trở lại xử lí văn bản:
Bây giờ chúng ta đã biết thuộc tính của kí tự và ta sẽ mở rộng chương trình xử lí văn bản bằng cách thêm vào việc chèn và huỷ bỏ kí tự ,đổi màu.
Chương trình 4-7 :
// Memory_7
#include <conio.h>
#include <dos.h>
#define colmax 80
#define rarrow 77
#define larrow 75
#define video 0x10
#define norm 0x07
#define blue 0x01
#define bkspc 8
#define altu 22
#define ctrlc '\x03'
int col=0;
int length=0;
int far *fptr;
union REGS reg;


void cursor() {
    reg.h.ah = 2;
    reg.h.dl = col;
    reg.h.dh = 0;
    reg.h.bh = 0;
    int86(video, & reg, & reg);
}


void insert(char ch, char attr) {
    int i;
    for (i = length; i > col; i--)
        * (fptr + i) = * (fptr + i - 1);
    *(fptr + col) = ch | attr << 8;
    ++length;
    ++col;
}


void del() {
    int i;
    for (i = col; i <= length; i++)
        * (fptr + i - 1) = * (fptr + i);
    --length;
    --col;
}


void clear() {
    int j;
    for (j = 0; j < 2000; j++)
        * (fptr + j) = 0x0700;
}


int main() {
    char ch, attr = norm;
    void clear(void);
    void cursor(void);
    void insert(char, char);
    void del(void);
    fptr = (int far * ) 0xB8000000;
    clear();
    cursor();
    while ((ch = getch()) != ctrlc) {
        if (ch == 0) {
            ch = getch();
            switch (ch) {
            case rarrow:
                if (col < length) ++col;
                break;
            case larrow:
                if (col > 0) --col;
                break;
            case altu:
                attr = (attr == norm) ? blue : norm;
            }
        }
        else
            switch (ch) {
                case bkspc:
                    if (length > 0)
                        del();
                    break;
                default:
                    if (length < colmax)
                        insert(ch, attr);
            }
        cursor();
    }
    
    return0;
}

Khi gõ tổ hợp phím Alit+U sẽ lật biến attr qua lại giữa norm (thuộc tính 07) và blue(cho chữ màu xanh - thuộc tính 01).
Hàm insert(O có vòng lặp for dùng để thâm nhập trực tiếp bộ nhớ và con trỏ far để dịch các kí tự sang trái khi cần chèn. Tiến trìmh dịch phải bắt
đầu từ cuối câu để tránh ghi đè lên.

0 bình luận:

Đăng nhận xét