Thông báo

Collapse
No announcement yet.

Một thí nghiệm về RTOS

Collapse
X
 
  • Lọc
  • Giờ
  • Show
Clear All
new posts

  • Một thí nghiệm về RTOS

    Lời mở đầu,
    Đã từ rất lâu tôi có một sự tò mò to lớn về RTOS, bí ẩn đó cứ luẩn quẩn trong đầu tôi mãi. Cho đến những ngày gần đây, khi sắp xếp được công việc và tích lũy được chút hiểu biết, tôi quyết định đầu tư thời gian để làm sáng tỏ về nó. Tôi không nắm rõ hiện tại có ai đã phát triển hay có một nghiên cứu về nhân RTOS chưa, nếu có hi vọng chúng ta sẽ có những trao đổi thú vị. Một vấn đề khác mà tôi cũng tò mò rằng thực trạng ứng dụng RTOS vào các sản phẩm của công ty Điện tử Việt nam ra sao? Đó cũng là một khía cạnh hay mà mọi người có thể chia sẻ. Có 2 ghi chú về mã nguồn mong mọi người chú ý: tác giả không chịu trách nhiệm về bất cứ thiệt hại nào gây ra bởi việc sử dụng mã nguồn này trong sản phẩm; thứ 2 là mã nguồn được phát hành dưới "THE BEER-WARE LICENSE (revision 42)".
    @admin: tại vì không thấy chủ đề nào về RTOS nên đăng ở mục AVR, ad thông cảm.
    Bài viết sẽ tương đương 14 trang A4 nên tôi sẽ tải lên dần trong 3 ngày để mọi người có thời gian nghiên cứu.


    -----------------------------------------------------------------------------------
    Một thí nghiệm về RTOS

    Thí nghiệm tập trung vào tìm cách thực hiện đổi ngữ cảnh trong RTOS trên Vi điều khiển đây chính là vấn đề cỗi lõi để hiện thực hóa khả năng chạy song song các tác vụ. Thí nghiệm không hướng vào các kỹ thuật lập lịch mà chỉ sử dụng một phương pháp đơn giản nhất là Round Robin.
    1. Vấn đề
    Mục tiêu là chạy hai hàm có vòng lặp vô tận trên AVR một cách song song mà chúng không hề nhận biết về cá thể khác. Vi điều khiển được sử dụng là ATMega8, kiểu lập lịch được sử dụng là Round Robin, Timer 0 được sử dụng để định thời lịch.
    void counter_1s(){
    while(1) tăng_port_B_sau_mỗi_1s();
    }
    void passing_io(){
    While(1) đọc_port_C_ghi_sang_port_D();
    }
    OS này được đóng gói trong LabRTOS.c với các bước khởi tạo như các RTOS khác:
    void main(){
    TaskData td_cnt;
    TaskData td_pio;
    LabRTOS_init();
    LabRTOS_newTask(&counter_1s, &td_cnt);
    LabRTOS_newTask(&passing_io, &td_pio);
    LabRTOS_start();
    //never return
    }
    Cụ thể hơn, Port B được yêu cầu tăng giá trị thêm 1 sau mỗi 1 giây, khi đến 255 thì trở về 0. Port D luôn cập nhật giá trị logic từ port C sau mỗi 1ms.

    ----------------------------------------
    Tối qua định vào viết tiếp phần 2 mà có hơn 1000+ tv online. Page load mãi không được, đành trễ hẹn vài tiếng đồng hồ vậy. Có một điều thú vị là LabRTOS viết hoàn toàn bằng C (+1 chút thủ thuật), nguyên lý cơ bản là thay đổi SP và tính toán giá trị PC khởi đầu. OK, không dài dòng nữa, của bạn đây!
    ----------------------------------------

    2.Phân tích & ý tưởng:

    Ngắt Timer 0 được sử dụng để chuyển tác vụ sau mỗi 1ms. Vai trò của ISR Timer 0 như sau: khi được gọi, copy thái hiện thời của MCU (các regs bao gồm PC), copy các giá trị này vào vùng nhớ tương ứng (stack) với counter_1s, lật đến tác vụ tiếp theo trong danh sách, phục hồi lại trạng thái của MCU, trao quyền thực thi (nạp giá trị PC*) cho tác vụ mới.

    *nạp PC thực hiên gián tiếp qua stack, không trực tiếp gán giá trị này.

    <hinh1_exe>
    Click image for larger version

Name:	hinh1_exe.JPG
Views:	1545
Size:	33.3 KB
ID:	1646135
    Các tác vụ chuẩn bị được chạy được chứa trong cấu trúc danh sách liên kết vòng (Round Robin).

    <hinh2_data_st>
    Click image for larger version

Name:	hinh2_data_st.JPG
Views:	1535
Size:	25.0 KB
ID:	1646136
    Trường td_pio.next và td_cnt.next trỏ chéo đến nhau. Sẽ không mất nhiều sức để quyết định tác vụ nào được thực hiện tiếp, chỉ cần gọi next. Do stack AVR sẽ chạy từ cao xuống thấp nên giá stack_ptr ban đầu sẽ khởi tạo từ vị trí cao và giảm dần khi hoạt động. Cấu trúc TaskData thích hợp nên là:
    typedef struct st_TaskData{
    struct st_TaskData * next;
    uint8* stack_ptr;
    uint8 stack_data[64];
    } TaskData;
    Toàn bộ dữ liệu của tác vụ đang chạy được lưu trong TaskData. Vị trí hiện tại của PC thì không được lưu trữ tường minh trong cấu trúc này vì ta sẽ không thao tác trực trên PC bởi giá trị này được cất và lấy ra từ stack một cách tự động bởi MCU khi ngắt sảy ra. Ý tưởng là chỉ cần thay đổi MCU Stack Pointer khi chuyển ngữ cảnh.
    Theo dõi hoạt động của một MCU Stack:

    <hinh3_stack_isr> Click image for larger version

Name:	hinh3_stack_isr.JPG
Views:	1533
Size:	37.0 KB
ID:	1646137




    Khi một ngắt sảy ra, giá trị PC + 1 sẽ được đẩy vào stack trước tiên bởi phần cứng, sau đó CPU sẽ nhảy tới bảng vector ngắt nơi chứa lệnh gọi hàm phục vụ ngắt (ISR). Việc đầu tiên mà ISR - viết bởi C làm là lưu các thanh ghi vào stack. Giãn cách (trừ từ) SP một khoảng đúng bằng kích thước local data của nó. Như vậy kích thước SP giảm đi một khoảng: sizeof(PC) + No_of_saved_Regs + sizeof(ISR_Ldata). Cuối giai đoạn thực hiện ISR, SP được co lại (cộng thêm) khoảng bằng local data của nó, các lệnh pop kéo dữ liệu từ stack về lại các thanh ghi (trừ PC). Sau cùng lệnh reti được gọi, phần cứng sẽ nạp lại giá trị PC từ stack. Chương trình bị ngắt sẽ thực thi từ lệnh kế tiếp.
    /*first, HW pushed PC*/ push R0
    push R1
    sub SP, sz(ISR_local_data) … add SP, sz(ISR_local_data)
    pop R1
    pop R0
    reti
    /*then, HW pop PC*/
    Ý tưởng chuyển ngữ cảnh:


    <hinh4_idea> Click image for larger version

Name:	hinh4_idea.JPG
Views:	1544
Size:	72.6 KB
ID:	1646138


    --------------
    Ok, như đã hứa, toàn bộ tài liệu đã ở đây!
    --------------

    Cấu trúc TaskData chứa toàn bộ dữ liệu của tác vụ gắn với nó, bao gồm địa chỉ lệnh hiện hành (ở chân stack), .stack_data[64], đỉnh stack (.stack_ptr). Thêm vào đó, để đảm bảo tính đơn giản cho thí nghiệm, TaskData sẽ chứa luôn thông tin lập lịch đó là liên kết tới task tiếp theo sẽ được chạy theo lịch (.next).
    Khi tạo tác vụ, cần thực hiện các việc sau:
    1. Đưa địa chỉ hàm tác vụ vào chân stack (tại 2 địa chỉ .stack_data[62:63]). Khi ISR trả về (reti), giá trị này nghiễm nhiên được phần cứng nạp vào PC, tác vụ sẽ bắt đầu được gọi thực thi.
    2. Tính toán .stack_ptr sao cho khi ISR trả về, SP của CPU được kéo lại đúng tại vị trí chứa địa chỉ hàm tác vụ. Giá trị này tính bằng công thức:
    .stack_ptr =
    địa_chỉ_cuối_của_cấu_trúc_TaskData
    TRỪ Kích_thước_local_data_của_ISR_TMR0
    TRỪ Kích_thước_C_dùng_lưu_trữ_thanh_ghi
    TRỪ Kích_thước_kiểu_function_pointer


    *Trong đó Kích_thước_local_data_của_ISR_TMR0 là hằng số có thể được xác định bằng cách deassemble ISR và xem stack của nó bị trừ đi bao nhiêu đó chính là kích thước của local data cần bởi ISR. Kích_thước_C_dùng_lưu_trữ_thanh_ghicũng được xác định bằng số thanh ghi được push vào stach. Hàm ISR cần được viết cẩn thận sao cho ở mọi chế độ biên dịch optimize sẽ không làm các tham số này bị thay đổi. Dùng asm có thể là một lựa chọn tốt. Kích_thước_kiểu_function_pointer chính là nơi chứa địa chỉ hàm tác vụ trước khi chạy (1).

    3. [Thêm] Như đã nói, vì tính đơn giản nên địa chỉ của cấu trúc TaskData tiếp theo sẽ được lưu tại .next.

    Ngắt T1: Nạp tác vụ đầu tiên của chuỗi tác vụ.
    Có 2 việc cần làm:
    1. Chỉ ra tác vụ hiện hành.
    td_running = &td_pio;

    2. Nạp SP với giá trị .stack_ptr của tác hiện hành sau đó thoát ISR. Khi này HW stack sẽ trỏ tới vùng stack_data của tác vụ. Sau hàng loạt lệnh pop để trả giá trị từ stack về thanh ghi, stack sẽ bị co lại tới vị trí chứa địa chỉ hàm tác vụ, lệnh reti cuối cùng sẽ nạp địa chỉ hàm này vào PC. Tác vụ đầu tiên của chuỗi được thực hiện.
    SP = td_running->stack_ptr;
    Return;
    Ngắt T2 trở đi: chuyển đổi giữa các tác vụ.
    Có 3 việc cần làm:
    1. Lấy TOS của tác vụ vừa bị ngắt và lưu.Giá trị SP sẽ được gán trực tiếp vào .stack_ptr của task hiện hành mà không phải quan tâm đi không gian dữ liệu của hàm ngắt. Điều này bởi vị sau khi chuyển SP ở bước (3) stack_ptr sẽ bị co lại một lượng bằng như thế.
    td_running->stack_ptr = SP;

    2. Chỉ ra tác vụ hiện hành mới.
    td_running = running->next;

    3. Trỏ SP sang tác vụ mới.
    Khi rời khỏi ISR, SP sẽ bị co lại một lượng cố định, các lệnh pop sẽ nạp lại dữ liệu cho các thanh ghi đúng như khi chúng bị ngắt. Sau cùng lệnh reti sẽ phục hồi lại vị trí mà tác vụ này đang thực hiện.
    SP = td_running->stack_ptr;
    Return;
    3. Code
    <Đính kèm>

    4. Những Thay đổi trong thực tiễn & gợi mở
    1. Công thức tính TOS khi khởi tạo:
    td->stack_ptr =
    (uint16)td
    + sizeof(TaskData)
    - sizeof(vfunc)
    - PRIV_ISR_STACK_SAVING_REGS_USAGE_SIZE
    - PRIV_ISR_STACK_LOCAL_DATA_USAGE_SIZE
    - 2; //TODO: why??
    • Vấn đề 1 (BỎ NGỎ), điều kiện khi chỉ dùng C ISR là PRIV_ISR_STACK_LOCAL_DATA_USAGE_SIZE=0 do khi có local data trong ISR, AVR gcc sẽ lưu stack vào r29:r28 sau đó sẽ phục hồi lại SP từ cặp này, thao tác này nằm ngoài nội dung code C trong ISR nên không thể can thiệp. Do đó lúc chuyển task, SP sẽ được nạp giá trị rác từ r29:r28 từ lần tính nào đó.
    /*3. restore new task status */
    SP_H = td_running->SPRegs.High;
    160: 83 81 ldd r24, Z+3 ; 0x03
    162: 8e bf out 0x3e, r24 ; 62
    SP_L = td_running->SPRegs.Low;
    164: 82 81 ldd r24, Z+2 ; 0x02
    166: 8d bf out 0x3d, r24 ; 61
    }
    }
    168: 25 96 adiw r28, 0x05 ; 5
    16a: de bf out 0x3e, r29 ; 62

    16c: cd bf out 0x3d, r28 ; 61
    16e: cf 91 pop r28
    170: df 91 pop r29
    Trên là mã assembly của ISR khi có sự xuất hiện của local data với kích thước 5.
    • Vấn đề 2(ĐÃ RÕ), tại sao lại có “Trừ 2” trong công thức. Tôi đã phải dành mất một đêm mới khám phá ra điều thú vị đó. Năm phút trước tôi không có bất kỳ ý tưởng nào. Ơ rê ca! Thì ra thì ra là sẽ có những 2 lần PC được lưu trong Stack khi ngắt. Lần 1: khi timer 0 tick -> AVR Core lưu PC+1 vào stack; lần 2: ngay sau đó nó nhảy đến bảng vector ngắt thứ mà chứa lệnh rjmp tại __vector_9 -> PC+1 được lưu bởi sự thực thi lệnh này. Giờ thì sáng như ban ngày rồi
    2. Điều gì nếu một task không chứa vòng lặp vô tận? (BỎ NGỎ)

    Uoa! chương trình của chúng có vẻ hỗ trợ song song khá tốt khi 2 vòng while(1) cùng chạy. Nhưng nếu một task không có vòng lặp vô tận thì sao? Lỗi vô cùng nghiêm trọng sẽ xảy ra, chắc chắn.
    Task đó sẽ nhả Stack ra, nhường phần thời gian còn lại trong chu trình của nó cho vòng while(1) trong hàm LabRTOS_start() ? Không. Bởi vì SP đã được chuyển sang vùng dữ liệu của Tác vụ đó, khi Stack trả về hết, SP sẽ đứng ở cuối của cấu trúc TaskData, kế đến lệnh ret sẽ nạp một giá trị rác phía sau vào PC. Tiếp theo, đến một thời điểm định trước, TaskData của Tác vụ này lại được tham chiếu đến trong ISR, thật nguy hiểm khi dữ liệu đã trở thành đống RÁC, cần một thứ gì đó khởi tạo lại TaskData cho tác vụ này khi nó vừa thoát khỏi, một kiểu giống như hàm LabRTOS_newTask().

    Để tránh giá trị rác cho PC, có thể thêm một function pointer vào sau cùng cấu trúc TaskData mà trỏ tới hàm lặp vô tận nào đó. Để tránh RÁC ta cũng có thể dùng hàm này để khởi tạo lại TaskData trước khi nó lặp vô tận. LƯU Ý: hàm này không phải dùng Stack data và là reentrance để tránh tràn stack do nó có khả năng bị đệ quy nhiều lần (thực thể 1 đang chạy, ngắt TMR0, chạy thực thể 2, ngắt… cứ thế Stack sẽ sớm tràn).

    3. Vấn đề cuối nhưng không phải là hết đó là NGẮT (BỎ NGỎ)

    Chặn và cho ngắt hoạt động một cách an toàn mà không ảnh hưởng đến việc chuyển ngữ cảnh là một việc cần có sự tính toán và thực hành nhiều hơn nữa.

    4. Các dịch vụ gia tăng (BỎ NGỎ)

    Nếu như các vấn đề trên đã được giải quyết, và RTOS được kiểm thử kỹ lưỡng và đảm bảo tính an toàn, lúc này ta hãn suy nghĩ về việc gia tăng giá trị cho nó bằng các dịch vụ chia sẻ tài nguyên như: Lock, Semaphore, Pipe, Message Copy,.. Áp dụng các kỹ thuật lập lịch phức tạp hơn, triển khai Worker, Timer… Tiêu chuẩn hóa các hàm API sau đó porting lên các kiến trúc khác, tạo một bản mô phỏng trên cho máy tính. Đó là chính là chu trình phát triển của một RTOS. Cũng chính những thứ ấy đã được tích hợp trong một RTOS hoàn chỉnh nên làm chúng ta cảm thấy RTOS là thứ đồ sộ và ghê gớm. Cuối cùng thì việc thực hiện chuyển ngữ cảnh mới là trái tim của RTOS, những thứ khác thường được phát triển như là các mô-đun dịch vụ đi kèm được thêm vào sau khi “trái tim” đã hoàn thiện. Đến đây thì công việc cũng hoàn thành tương đối, cũng thấy khoan khoái hẳn.
    5. Vài ghi chú cho việc biên dịch

    Khi biên dịch trong AVR Studio 5.0, chọn cấu hình Release và cho xuất hết các dạng file để khám phá.
    <hinh5_config1>

    Chỉnh cờ build để xem chế độ mix code asm/C -Wa,-adhln –g
    <hinh6_config2>

    <hinh7_config3>

    <hinh8_config4>

    Khi build lần 1 và mở file .s để xem thông tin về stack được dùng trong ISR
    <hinh9_view_asm>

    Giá trị frame size stack size được dùng để tính giá trị
    <hinh10_modify_c>


    Sau đó build lại lần 2.
    Sau đó đem file hex đi nạp vào ISIS, tất nhiên J
    Mạch test (chưa test live, mới mô phỏng). Thử chạy với nhiều hơn 2 tasks!
    <hinh11_test_circuit>
    --------
    vì bị giới hạn attachment nên, các hình này được chứa trong .zip file đính kèm, bao gồm project, code và test board.

    [ATTACH]n1646278[/ATTACH]
    --------

    Kết: Cảm thấy như tìm được một hành tinh mới! Ban đầu định dùng SG8V1 để minh họa cho mọi người nhưng mà không có KIT nên trở lại với AVR quen thuộc. Hi vọng bài viết này hữu ích cho mọi người!
    Thân ái,

  • #2
    Cái OS này có gì hay hơn FreeRTOS không bạn?
    AVR đã quay trở lại: ATMEGA32: 66k, ATMEGA8A: 30k, ATMEGA48: 30k.
    Xem thêm tại Online Store ---> Click here
    Mob: 0982.083.106

    Comment


    • #3
      Không. Ngay từ cái ý định đầu tiên tôi đã không định xây dựng thí nghiệm này để cạnh tranh với bất cứ RTOS nào khác. Chỉ để hiểu cách mà context switch hoạt động trong một preemptive multitasking OS. Nếu mà có nhiều thời gian hơn nữa, tuyệt đối tôi sẽ làm hẳn 1 cái RTOS hoàn thiện cho mình

      Comment

      Về tác giả

      Collapse

      dfjnan Tìm hiểu thêm về dfjnan

      Bài viết mới nhất

      Collapse

      Đang tải...
      X