Lập trình C - Hỏi về hàm delay cho vi điều khiển

Chào mọi người!
Em có vấn đề thắc mắc mong được mọi người trả lời.
Em đang viết hàm delay_s (giây) cho MCU 8051 sử dụng thạch anh 12MHZ.
Thì em được biết khi mình cho biến có kiểu dữ liệu cần byte lớn thì thời gian xử lý lệnh sẽ tăng lên.
Ví dụ :

  • Biến loop kiểu unsigned int (2 byte) chạy vòng lặp 21738 lần thì sẽ delay được khoảng 0,1s.
  • Biến loop kiểu unsigned long (4 byte) chạy vòng lặp 21738 lần thì sẽ delay được 1s.

Nhưng em đang thắc mà là tại sao khi cho 2 biến có cùng kiểu dữ liệu thì thời gian xử lý lệnh sẽ khác nhau như trường hợp dưới ( em chưa biết thuật ngữ nào để diễn tả :3)
Ví dụ:

  • Biến loop kiểu unsigned int chạy vòng lặp 32767 hoặc dưới giá trị này ( có giá trị 15 bit) thì thời gian lặp khoảng 0,1s.
  • Biến loop kiểu unsigned int chạy vòng lặp 32768 ( có giá trị 16 bit) thì thời gian lặp hơn 1s. ( Chênh lệch nhiều)

Code mẫu về delay em sử dụng:

void delay_s(unsigned int time) //Ham delay giay
    {
        unsigned long loop;    //21738 loop thi duoc 1s sai so 0%
        while(time>0)    
        {
            loop=0;        
            while(loop < 21738)
            {
                loop++;    
            }
            time--;
        }
    }

Giả sử khi compile thì dòng loop++ sẽ được dịch thành 1 lệnh assembly ADD . Mình tham khảo thì thấy MCU 8051 lệnh ADD có 2 loại theo kích thước của opcode: 1 byte2 byte. Với biến có size lớn hơn thì có thể lệnh ADD lúc này là 2 byte, và với một lệnh có kích thước lớn thì thời gian chạy cũng sẽ lâu hơn.

Mình chưa bao giờ làm việc với cho chip này nên trên đấy là suy đoán của mình về trường hợp này, nếu có điểm nào ko hợp lí thì mọi người góp ý để có câu trả lời đúng nhé :slight_smile:

4 Likes

Trên bài post có 2 ý.

Ý thứ nhất: Khi thay kiểu dữ liệu của biến loop từ 2 bytes sang 4 bytes thì thời gian chạy lâu hơn?
Lý do là chip 8051 là chip 8-bit (1 byte). Khi muốn xử lý số có kích thước lớn hơn 1 byte thì vi điều khiển phải lưu biến ở 4 địa chỉ liên tiếp thay vì lưu vào một địa chỉ ô nhớ. Khi thực hiện các phép toán trên những biến kiểu dữ liệu lớn thì vi điều khiển phải tốn nhiều lệnh assembly hơn là những biến có kiểu dữ liệu nhỏ. Do đó thời gian chạy cũng lâu hơn.
Với thạch anh 12MHz thì 8051 sẽ có tốc độ là 1 triệu instruction cycle/ 1 giây. Như vậy toàn bộ khối lệnh trong loop sẽ được disassembly ra 46 instruction cycle. Lưu ý: 1 lệnh (opcode) có thể tốn từ 1 tới 2 instruction cycle. Bạn thử tìm kiếm file ASM (chỉnh output ra file ASM cho compiler).

Ý thứ hai: Tại sao 2 biến có cùng kiểu dữ liệu thì thời gian xử lý lệnh sẽ khác nhau với giá trị khác nhau?
Ở đây biến 16-bit có giá trị max là 65,535 nên không có lý do gì rõ ràng để nó bỗng nhiên chậm hơn nhiều. Trừ khi compiler thực hiện một số việc optimize nào đó. Ở đây chỉ có thể tìm hiểu sâu hơn nhờ đọc file ASM của mỗi code tương ứng thôi.

Nói thêm, việc dùng phương pháp delay blocking dựa trên opcode là một phương pháp khá phổ biến khi lập trình 8051. Tuy nhiên, bạn không nên dùng code C trong vòng lặp để tránh phụ thuộc vào compiler. Hoặc nếu dùng thì nên dùng nhiều vòng lặp của biến 8-bit. Tuy tình huống và phiên bản compiler sẽ biên dịch từ code C ra số lượng opcode khác nhau.
Cách an toàn nhất cho phương pháp này là nhúng code asm vào code C. Ví dụ: http://www.circuitstoday.com/software-delay-routine-in-8051

Cách tốt hơn cho delay là dùng hardware timer.

4 Likes

Cảm ơn bạn nhiều! mình có thực nghiệm và đo số liệu thì nó đúng như trên

Cảm ơn bạn , mình sẽ tham khảo các code ở link để nhúng vào.
Về ý thứ 2 mình có thực nghiệm rồi đo kết quả.
Thì biến loop mà chứa giá trị >= 16 bit thì thời gian thực hiện lệnh sẽ gấp 8 lần so với biến loop chứa giá trị <16 bit với cùng 1 kiểu dữ liệu.

Bạn ly có trả lời câu hỏi của mình và thấy ý rất khớp so với kết quả mình đo được.
Bạn có thể giải thích rõ hơn ý thứ 2 không.

Về ý thứ nhất : ở đoạn “xử lý số có kích thước lớn hơn 1 byte thì vi điều khiển phải lưu biến ở 4 địa chỉ liên tiếp thay vì lưu vào một địa chỉ ô nhớ”.

Mình mới học C cũng như MCU nên chưa hiểu rõ ý này lắm bạn giải thích rõ hơn giúp mình nhé :smiley:

Về ý thứ nhất: MCU 8 bit có nhiều định nghĩa, có thể dựa trên bus hoặc trên data hoặc mix nhiều thứ khác nhau, ví dụ PIC16 nó là vi điều khiển 8 bit nhưng dùng bus 16. Tạm bỏ qua mấy thứ phức tạp đó. Lấy kiến trúc máy tính cực kỳ đơn giản để hình dung như sau: https://en.wikibooks.org/wiki/Microprocessor_Design/ALU#Example:_4-Bit_ALU
ALU là bộ thực hiện phép toán cộng trong MCU. Ví dụ ALU 2-bit, 4-bit, 8-bit.
Trong link ở trên là hình vẽ về việc cộng 2 số A và B cùng có 4-bit.
Với ALU 8-bit thì chỉ cộng được 2 số 8-bit một lúc. Nếu muốn cộng 2 số 16-bit thì phải thực hiện 2 lần. Lần 1 ở Byte thấp, lần 2 ở Byte cao. Như vậy có thể hình dung hú họa là thực hiện cộng 2 số 16-bit sẽ tốn ít nhất gấp đôi thời gian thực hiện cộng 2 số 8-bit.

Về ý thứ hai:
số unsigned 16-bit có range từ 0 tới 65535
số unsigned 15 bit có range từ 0 tới 32767
Nếu range không vượt các mốc 8-bit, 16-bit, 24-bit, 32-bit thì mình không nghĩ là asm code sẽ khác nhau. Trừ khi con chip dùng bit thứ 16 để làm chuyện gì khác.

Bạn có thể post code của 2 sự thay đổi lên đây để mình review. Có thể code của bạn không đúng với sự mô tả của bạn.

5 Likes

Cảm ơn bạn về ý thứ nhất, mình sẽ tìm hiểu thêm :smiley:
Bạn xem lại code của mình, cũng như kết quả debug nhé, mình debug trên keil C

CODE vòng lặp

#include "main.h"

sbit LED = P2^1;

void delay_s(unsigned int time) //Ham delay giay
	{
		unsigned int loop;	
        while(time>0)	
        {
            loop=0;		
            while(loop < 32767)
            {
                loop++;	
            }
            time--;
        }
	}

void main()
{
	while(1)
	{
		LED = 1;
		delay_s(1);
		LED = 0;
		delay_s(1);
	}
}

2 ảnh lần lượt của các kết quả debug.

Mấu chốt có thể nằm ở dòng while(loop < 32867) và while(loop < 32768).
Ở đây chưa rõ 32767 và 32768 IDE hiểu cụ thể là gì. Tôi nghĩ nó đều là signed.
Nếu như vậy, 32767 có thể là signed int (16bit) nhưng 32768 lại là signed long (32 bit).
Chắc ăn có thể kiểm chứng bằng cách di chuột vào con số 32767 và 32768 xem IDE báo nó là loại gì (nếu IDE có chức năng đó).
Rõ ràng việc so sánh loop và 32767 đều là 2 số 16 bit sẽ nhanh hơn việc so sánh 1 số 16 bit và 1 số 32 bit rất nhiều (có thể phải dùng biến tạm để chuyển đổi loop lên 32 bit mới có thể so sánh được). Dẫn tới thời gian sẽ sai khác nhiều.

4 Likes

hình này có phải debug đâu :V

down keil c về thử thì ctrl+F5 debug ra thế này:

với 32767 thì vòng lặp loop++ nó optimize thành do while chỉ có 5 dòng nhưng lúc chạy đa số chỉ có 3 dòng :V vì nó đa số chỉ compare 1 byte thấp nhất

với 32768 thì compiler nó hiểu là số nguyên 32-bit 0x00008000, nó phải load giá trị này lên 4 register r0-r3 :V rồi so sánh 4 byte trong hàm C?SLCMP tổng cộng với load 0x00008000 lên register thì tới gần 30 dòng nên chậm hơn 32767 chỉ chạy ~3 lệnh 1 vòng lặp nên chậm hơn ~10 lần.

sửa lại thành 32768U thì nó hiểu là unsigned 16 bit 0x8000, optimize còn 7 dòng, chạy đa phần chỉ có 4 dòng, vẫn chậm hơn 32767 kia có lẽ là 33% :V Sửa 1 số mà chậm hơn 33% thì sao làm hàm delay được. Hơn nữa lúc delay nó vẫn phải tính toán nhiều, tìm doc xem có hàm sleep() gì ko nếu ko thì chèn mã máy noop là no operation gì đấy cho cpu đừng tính toán gì.

4 Likes

Debug trên Keil C có nhiều phần ấy @tntxtnt , cái này chuyên cho MCU nên mình có thể hiển thì debug các phần mong muốn như : register, asm, clock…
@Duong_Act như anh nghĩ rồi ạ :smiley: , bạn @tntxtnt giải thích code ASM là đúng với vấn đề trên :smiley:
@tntxtnt có thể giải thích giúp mình đoạn “32768U”, là ép kiểu hả bạn, mình mới biết đến ép kiểu cho biến , còn đây là cho số @@

Hằng số nguyên mặc định là có dấu nên thấy 32768 nó lấy 32 bit luôn.
Còn 32768U có kiểu unsigned int.

4 Likes

số bình thường nó hiểu là int, mà int ở đây là 16 bit. 32767 int 16-bit chứa được 32767 nên ko có vấn đề gì cả. 32768 nó ko chứa được nên có lẽ nó hiểu là long int hay long long int mà ở đây chắc là 32-bit :V

image

thấy int chứa ko đủ thì nhảy tới long int, long int ko đủ thì tới long long int (C99)

U hay u ở đây là unsigned-suffix của integer constants :V
https://en.cppreference.com/w/c/language/integer_constant

cách viết integer constants có 3 phần:

  • phần cơ số: optional - default hệ 10. Các giá trị có thể là:
    • 0x cho hệ 16
    • 0 cho hệ 8
    • 0b cho hệ 2 (C23 mới có :V)
  • phần giá trị: tùy vào cơ số mà là ký tự từ
    • hệ 10: 0-9
    • hệ 16: 0-9a-f
    • hệ 8: 0-7
    • hệ 2: 0-1
  • phần đuôi: báo kiểu của số nguyên này. Optional - default tùy vào giá trị như hình trên :V Có 2 loại (có thể viết riêng lẻ 1 trong 2 loại hoặc kết hợp của 2 loại thành ull hay UL v.v…) là
    • unsigned-suffix (U hoặc u)
    • long-suffix (L, l, LL, ll)
4 Likes

Mình đã hiểu cảm ơn bạn nhiều :smiley:

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?