CI của gitlab

Đến thời điểm hiện nay, tôi thường yêu cầu các dự án của mình phải có CI. Nếu viết bằng script như nodejs thì CI sẽ có nhiệm vụ check syntax, để các thành viên trong nhóm cùng một kiểu viết giống nhau, và chạy các mức test khác nhau như unit test, CDC test.

Điểu tuyệt vời của gitlab là nó đi kèm với bộ CI miễn phí cho cả dự án private (yay). Blog này tôi sẽ trình bày chiến lược để thiết lập env test trên Gitlab CI để build docker image, và upload nó lên gitlab registry.

 

Gitlab runner

Là nơi chương trình test của bạn sẽ được chạy. Bạn có thể có một PC riêng biệt cho việc build và test. Gitlab cung cấp một chương trình khá tiện https://gitlab.com/gitlab-org/gitlab-ci-multi-runner để thiết lập runner cục bộ. Cá nhân tôi chỉ sử dụng multi runner này khi chương trình cần tài nguyên lớn.

Phần lớn thời gian tôi sử dụng shared runner https://about.gitlab.com/gitlab-com/settings/#shared-runners. Xin cảm ơn sự tử tế của Digital Ocean khi cho mỗi build một tài nguyên 4GB chạy trong mỗi máy ảo riêng biệt.

Shared runner chạy trên docker, nên có thể khai báo các docker có sẵn khá tiện (yay).

.gitlab-ci.yml

Là file cấu hình cho gitlab ci. Dưới đây là một ví dụ trong đó khai báo một trạng thái của test pipeline là unit test. Phần sau dựa trên giả thuyết tôi đang xây dựng một web app với nodejs, và chạy unit test với npm run test.

image: node:boron-alpine

stages:
 - unit_test

unit_test_job:
 stage: unit_test
 script:
 - npm install
 - npm run test

Với mỗi git push lên CI được trigger. CI sẽ pull về docker image node:boron-alpine giống với docker chạy trong staging và production environment của web app này. Stages khai báo các bước của CI. Trong trường hợp này, tôi chỉ có một bước duy nhất là unit test khai báo trong unit_test_job.

Đơn giản như vậy.

Nhưng, vì tôi tin vào 12factor, và tôi muốn có quá trình build trước. CI hay staging hay production sẽ khác nhau ở env truyền vào docker như trình bày ở https://12factor.net/build-release-run. Tôi sẽ tạo một job để tạo artifact, và chạy test trên artifact này.

image: docker:1.12
services:
 - docker:dind

stages:
 - build

variables: 
 CONTAINER_TEST_IMAGE: registry.gitlab.com/company/project:$CI_BUILD_REF_NAME
before_script:
 - docker info
build_image_job:
 stage: build
 script:
 - docker build -t $CONTAINER_TEST_IMAGE .
 - docker run $CONTAINER_TEST_IMAGE npm run test

Setup này sử dụng docker trong docker (dind)  để build docker image trong một docker (nơi CI này đang chạy). Đều này đạt được do shared runner chạy với privileged enable (wao).

Như vậy lúc này CI sẽ tạo một docker có tên biến CONTAINER_TEST_IMAGE , và chạy test trong image này.

Tôi muốn tiến thêm một bước nữa, pipeline bao gồm: build, test, và release. Quá trình release xảy ra khi tôi tag git tag -a version, nó sẽ tạo ra tag latest và upload lên gitlab registry.

Khoa, gitlab registry? Đúng vậy, gitlab cho phép bạn lưu các image lên host của họ. Bạn sẽ có CI, và registry (miễn phí).

image: docker:1.12

services:
- docker:dind

stages:
- build
- test
- release
- deploy

variables:
 CONTAINER_TEST_IMAGE: registry.gitlab.com/company/project:$CI_BUILD_REF_NAME
 CONTAINER_RELEASE_IMAGE: registry.gitlab.com/company/project:latest

before_script:
 - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com

build:
 stage: build
 script:
 - docker build --pull -t $CONTAINER_TEST_IMAGE .
 - docker push $CONTAINER_TEST_IMAGE

unit_test:
 stage: test
 script:
 - docker pull $CONTAINER_TEST_IMAGE
 - docker run $CONTAINER_TEST_IMAGE npm run unit_test

cdc_test:
 stage: test
 script:
 - docker pull $CONTAINER_TEST_IMAGE
 - docker run $CONTAINER_TEST_IMAGE npm run cdc_test

release-image:
 stage: release
 script:
 - docker pull $CONTAINER_TEST_IMAGE
 - docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
 - docker push $CONTAINER_RELEASE_IMAGE
 only:
 - tags
 - triggers

Gitlab rất hào phóng khi cung cấp đầy đủ các công cụ và dịch vụ để bạn có thể xây dựng một quá trình CI khá hoàn chính, từ việc chạy các test đến việc lưu trữ các image của bạn.

Hy vọng bạn sẽ tìm thấy sự hữu dụng trong ecosystem của gitlab để xây dựng hệ thống bền vững.

Sử dụng AWS IoT

 

logo-iot

 

Trước đây tôi thường tận dụng mã nguồn mở để thiết lập MQTT broker để kết nối đến các thiết bị. Một số dự án mà tôi đã sử dụng có thể kể đến như Mosquitto, hay Mosca.

Với prototype, các chương trình này thực hiện rất tốt công việc của mình: thiết lập nhanh, và miễn phí😀.

Và đến mức độ nào đó, tôi gặp phải điểm yếu của chúng: Mosquitto không scale tốt, dù ai đó nói với tôi rằng hãy dùng chức năng bridge, tôi vẫn cho rằng nó không phải là các thức để scale. Mosquitto không cung cấp cơ chế authen/author rõ ràng. Mosca là một dự án khá nối tiếng về broker trên nền Node.js. Hình thức bảo mật của Mosca là thông qua username/pass cho mỗi kết nối. Vì viết bằng JS, nên khả năng mở rộng để tôi có thể hook function này quản lý mỗi kết nối khá tiện. Tuy nhiên Mosca như Mosquitto không thể cluster được.

Cuối năm 2015, AWS bổ sung gói IoT trong menu của họ, dưới dạng SaaS. Một số đặc điểm của AWS IoT có thể kể đến như:

  • scale, scale, scale.
  • bảo mật
  • liên kết dịch vụ khác, như lambda, dynamodb.

Thấy rằng AWS IoT đáp ứng được các nhu cầu cho một broker trong hệ thống kết nối, tôi viết lên vài dòng ở đây, xem như ghi chú của cá nhân vậy.

AWS IoT cung cấp gì

Bản thân là một SaaS broker, tôi sẽ sử dụng nó để kết nối đến các thiết bị thông qua MQTT. Vì nó cung cấp bởi AWS, broker này kỳ vọng sẽ ổn định, và hiện hữu trong một thời gian không ngắn.

AWS IoT định nghĩa một số thuật ngữ như:

  • Thing: là một đối tượng, tương đương với thiết bị nào đấy. Thing được kết nối với các principal thông qua policy.
  • Certificate: chứng chỉ X.509 mà các thực thể (principal) sử dụng để kết nối đến broker thông qua giao thức MQTT. Do đó, các client phải hỗ trợ SSL layer để làm việc với AWS IoT, tạo lớp bảo mật đầu tiên.
  • Policy: chính sách được áp dụng vào các Certificate tương ứng. Policy cung cấp cơ chế bảo mật tiếp theo, bằng cách quy định ai (mqtt client id) có thể connect được, và hình thức (pub/sub topic) làm việc.
  • Rule: để kết nối AWS IoT này với các dịch vụ khác, như đẩy dữ liệu qua Lambda hay lưu vào DynamoDB.

Có thể hình dung qúa trình tôi cần làm để kết nối một đối tượng (gọi D) của tôi vào AWS IoT, và sử dụng điện thoại (gọi A) để kết nối đến client này thông qua MQTT, trong đó tất cả các thông điệp đều được chuyển đến DynamoDB như sau:

  • Tạo hai Certificate, một cho A, một cho D.
  • Tạo một Thing, tôi đặt tên là doi-tuong-D.
  • Attach A và D vào thing doi-tuong-D.
  • Tạo hai policy. Bên dưới (*) là một ví dụ của policy cho phép mọi quyền kết nối và topic làm việc, gọi là policyA, policyD tương ứng. Attach policyA vào A, policyD vào D.

Lúc này nếu app A gởi (pub) lên command/req, thì thiết bị D sẽ nhận được khi nó đăng ký (sub) cũng trên cùng một chủ đề command/req.

Tôi muốn lưu các command này vào DB để xử lý về sau, tôi sẽ thiết lập một rule, gọi là ruleD. Rule được sử dụng để kích hoạt kết nối giua IoT và dịch vụ khác của AWS.

Đưới đây là mô hình diễn tả mối quan hệ của A và D.

blog-aws-iot-example

Tích hợp AWS IoT vào hệ thống

AWS cung cấp dashboard để tôi có thể thực hiện các bước trên, nhưng dĩ nhiên tôi không thể tiến hành bằng tay những công đoạn trên cho mỗi thiết bị/app được thêm vào. AWS cung cấp SDK (https://github.com/aws) để kết nối đến API của tất cả các dịch vụ của họ. Tôi sẽ xem AWS IoT như một serivce có nhiệm vụ làm broker.

Cuối cùng, tôi cần viết một microservice khác để quản lý các thing, cert, rule, và tạo các policy tương ứng với mức logic của ứng dụng.

Thay cho lời kết, cần lưu ý rằng Microsoft hay IBM đều cung cấp nhánh IoT trong dịch vụ đám mây của họ. Với nhiều sự lựa chọn như vậy, tôi nghĩ mình không nên nghĩ đến việc tự xây dựng lấy một broker trong hoàn cảnh hiện này.

(*) Một policy đơn giản cho phép hết các quyền kết nối và các chủ để pub/sub.


{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": [
            "iot:*"
        ],
        "Resource": ["*"]
    }]
}

 

 

npm install package from private repo

My company hosts most of the private repositories in gitlab, thanks to its generous unlimited-free-plan, and wonderful built-in CI.

npm need to get package from private gitlab, or bitbucket: create tag, and provide direct link when install package.

First: tag package with version (e.g. 0.0.1)

git tag -a v0.0.1 -m "first release"
git push origin v0.0.1

, then tell npm the link with tag. I will love to use git+ssh instead of git+https since my CI runner and collaborators already have the right to get access to this repo; thus don’t need to enter password every time npm try to install.

npm install git+ssh://git@gitlab.com:company/repo.git#v0.0.1 --save

Học FreeRTOS, p3

Bài này tiếp tục series học FreeRTOS, bàn về hàng đợi (queue), tiếp tục Phần 1Phần 2.

Trong FreeRTOS, mỗi task như là một chương trình riêng biệt, và để liên lạc giữa các task này, kernel cung cấp một số phương tiện có thể kể đến như queue, binary semaphores, counting semaphores, multex, và task notification. Sử dụng semaphores được đề cập ở những phần tiếp theo.

Đây là API đầy đủ của queue, một số hàm cơ bản về create, read, write, và delete tương ứng như sau cho các ví dụ ở learn_freertos/chapter2.

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
                            UBaseType_t uxItemSize );

BaseType_t xQueueSend( QueueHandle_t xQueue,
                       const void * pvItemToQueue,
                       TickType_t xTicksToWait );

BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void *pvBuffer,
                          TickType_t xTicksToWait );
void vQueueDelete( QueueHandle_t xQueue );

Đặt điểm queue trong FreeRTOS:

  • Một queue chứa một số hữu hạn các phần tử được khai báo khi khởi tạo. Quá trình khởi tạo chỉ không thành công khi vùng heap không còn đủ chổ trống.
  • Queue không được sở hữu bởi một task cụ thể nào cả, và được truy cập từ nhiều task. Thông thường một queue được ghi từ nhiều task, và được đọc ở một task nào đấy.
  • Queue hoạt động theo cơ chế FIFO (first in first out). Việc đọc thực hiện ở cuối queue, và việc ghi có thể ở đầu (xQueueSendToFront) hoặc đuôi (xQueueSendToBack). Một item khi được ghi vào queue thì giá trị của nó được sao chép lại, nghĩa là ta có thể free sau đó, hoặc sử dụng biến cục bộ cho việc ghi.
  • Block khi đọc queue. Khi một task ra lệnh đọc một queue, nếu queue đang trống nó sẽ đi vào chế độ Block và chờ. Task sẽ thoát ra khỏi chế độ Block khi một task khác hoặc interrupt service routine nào đó ghi vào queue này. Task cũng sẽ đi đến Ready nếu thời gian chờ kết thúc. Trong trường hợp nhiều task đang đọc, chỉ có một task được unblock, task đó có priority cao nhất. Nếu chúng cùng priority, task nào chờ lâu hơn thì sẽ được ưu tiên trước.
  • Block khi ghi queue. Khi một task ra lệnh ghi vào queue, nó sẽ đi vào chế độ Block nếu queue đang đầy. Task sẽ thoát ra khỏi Block khi queue có chổ trống trở lại, hoặc kết thúc thời gian chờ. Trường hợp nhiều task đang chờ để ghi thì task nào có priority cao hơn sẽ được unblock trước. Nếu cùng priority, task nào chờ lâu hơn sẽ được ưu tiên trước.

 

Ví dụ

blocking ở receiving

Nằm ở learn_freertos/chapter2/block_receiving.c. Chương trình làm việc như sau: 2 task ghi vào một queue kích thước 5 số long, một task khác đọc từ queue và in ra số long này.

Receiver của vReceiverTask có ưu tiên cao hơn, nên được thực thi trước, khi truy xuất đến queue thì nó nhảy vào trạng thái Block với timeout là 0 (chờ mãi mãi). Lúc này, vSenderTask với instance Task 1 được thực thi, và gởi số 100 vào queue. Ngay lập tức Receiver nhảy đến trạng thái Running, đọc từ queue, in ra terminal, rồi quay lại vòng lặp và việc đọc queue tiếp theo đặt task này vào trạng thái block lần nữa. Task 1 được resume, chạy tiếp đến taskYIED.

taskYIED báo với kernel rằng nó không chiếm đoạt CPU nữa, và kernel đặt Task 2 vào trạng thái Running. Quá trình tương tự như Task 1.

Kết quả ở terminal chạy file thực thi như sau:

Running as PID: 26812
Timer Resolution for Run TimeStats is 100 ticks per second.
Receiver: 100
Receiver: 200
Receiver: 100
Receiver: 200
Receiver: 100
Receiver: 200
Receiver: 100

Mô hình hoạt động các task này như hình bên dưới.

block_receiving

 

Để hiểu thêm, một số thông số sau có thể được thay đổi.

  • thay đổi `mainTASK_RECEIVER_PRIORITY` thành nhỏ hơn hoặc bằng `mainTASK_SENDER_PRIORITY`.
  • đổi `mainQUEUE_SIZE` thành 1, hoặc 10.
  • xóa lệnh `taskYIELD` trong `vSenderTask`.

blocking ở sender

Nằm ở learn_freertos/chapter2/block_sending.c. Chương trình này cũng giống chương trình ở trên, khác biệt là receive task có priority nhỏ hơn sending task.

Queue được khởi tạo để chứa mainQUEUE_SIZE = 3 struct xData. Quá trình chạy được diễn tả ngắn gọn như sau:

  • sender 1 đẩy vào queue, queue length = 1. `taskYield` cho phép sender 2 chạy.
  • sender 2 đẩy vào queue, queue length = 2. `taskYield` cho phép sender 1 chạy.
  • sender 1 đẩy vào queue, queue length = 3, queue đầy và không có khả năng nhận thêm dữ liệu nữa. `taskYield` cho phép sender 2 chạy.
  • sender 2 đẩy dữ liệu vào queue, nhưng không được. sender 2 chuyển sang trạng thái Blocking, với timeout là `xTicksToWait = 100ms`. sender 1 được kernel gọi lên thực thi.
  • sender 1 đẩy dữ liệu vào queue, nhưng không được. sender 1 chuyển sang trạng thái blocking. Kernel lúc này cho phép receiver task thực thi.
  • receiver task đọc 1 item từ queue, làm cho queue không còn đầy nữa. Sự kiện này trigger sender 2 task sống dậy (tại sao không phải là sender 1?)
  • sender 2 ghi tiếp một item vào queue, làm queue đầy trở lại. Sau đó nó gọi `taskYield` nhưng sender 1 đang ở trạng thái blocking nên chính sender 2 lại tiếp tục thực hiện, ghi item tiếp theo vào queue; việc queue đã đầy khiến sender 2 được đặt vào trạng thái blocking. receving task được kích hoạt.
  • receiving task đọc 1 item từ queue, ngay lập tức trigger sender 1 task nhảy sang trạng thái running. Quá trình giống sender 2, và được lặp lại vô tận.

Nếu đoạn trên dài quá thì bạn có thể xem hình sau.

block_receiving.PNG

Để hiểu thêm, bạn có thể thêm vào một task sender (sender 3 chẳng hạn).

 

Xác nhận: Các hình ở trên được lấy từ sách Using The FreeRTOS Realtime Kernel của Richard Barry.

Học FreeRTOS, p2

Phần 2 trong series này sẽ tiếp tục với phần quản lý bộ nhớ của FreeRTOS; tiếp tục sau Phần 1 đề cập đến việc tạo task.

Nếu lập trình trên PC, bạn sẽ quen thuộc với malloc, free để tạo, hủy một vùng nhớ. Ở trên hệ thống nhúng, việc sử dụng memory dynamic allocation không được khuyến khích. Các nguyên nhân có thể tóm gọn như sau:

  • Không xác định (not deterministic). Một hàm có thể được thực hiện/không tùy thuộc vào thời điểm gọi. Trong hệ thống RTOS điều này có thể khó chấp nhận khi không có người tương tác, và người thiết kế mong đợi chương trình luôn chạy theo kịch bản định sẵn.
  • Chương trình malloc/free có thể chiếm tài nguyên trong các hệ thống nhúng (phụ thuộc vào compiler) thường hạn hẹp RAM/ROM.
  • [Phân mảnh](http://www.design-reuse.com/articles/25090/dynamic-memory-allocation-fragmentation-c.html) bộ nhớ.
  • Một số implement malloc/free thường không thread-safe. Nếu chương trình multitask (đó là lý do bạn dùng FreeRTOS), bạn nên bảo vệ nó.

Do đó, khi viết chương trình nhúng, tôi hạn chế malloc/free.

Với kernel thì không có sự lựa chọn khác. FreeRTOS cần tạo/xóa vùng nhớ động mỗi khi các task, queue, hay semaphore được tạo/hủy. Đây là một phần của kernel.

FreeRTOS định nghĩa tổng số byte heap được cung cấp ở configTOTAL_HEAP_SIZE, trong file FreeRTOSConfig.h. Vùng heap này được dùng cho pvPortMallocvPortFree, hai implementation của malloc/free.

FreeRTOS cung cấp lớp quản lý bộ nhớ này độc lập, để kernel này chạy được với các nền tảng phần cứng khác nhau và trình biên dịch khác nhau, ở FreeRTOS/Source/portable/MemMang. Bạn có thể thấy tất cả các lựa chọn, gồm `heap_1.c, heap_2.c, heap_3.c, heap_4.c, heap_5.c’. Hoặc bạn có thể viết chương trình quản lý bộ nhớ của riêng mình🙂.

Theo mặc định của trong series này https://github.com/nqd/learn_freertos, heap_3.c heap_4.c được sử dụng (nguyên nhân ở bên dưới). Bạn có thể đổi dòng tương ứng ở dòng CFILES += heap_3.c port.c, Makefile.common để có sự lựa chọn khác.

Phần sau tôi sẽ trình bay sự khác nhau của các loại heap_* này.

 

Heap_1.c

Đơn gianr nhất, chỉ cung cấp malloc với hàm pvPortMalloc và không có vPortFree.

Selection_015

Hình trên diễn tả qúa trình các task chiếm dụng configTOTAL_HEAP_SIZE. A là lúc chưa có task nào được khởi tạo. B là lúc có một task được tạo. Mỗi task có stack riêng, quyết định bởi thông số đầu vào usStackDepth của hàm xTaskCreate. Mỗi task còn có vùng task control block (TCB) để làm đại diện cho task đó, xem thêm về TCB.

Như vậy số heap trống sẽ được xác định rõ ràng như trên hình, và lấy được thông qua hàm xPortGetFreeHeapSize.

Kết luận: Nếu chương trình của bạn không hủy task, queue, semaphore, hay gọi malloc, task_1.c là sự lựa chọn cho quản lý bộ nhớ.

 

Heap_2.c

Phức tạp hơn heap_1, với cơ chế free đơn gianr.

Heap_2 sử dụng tốt nhất khi pvPortMalloc sử dụng lại một lượng bộ nhớ vừa đúng với kích thước đang còn trống. Ví dụ trong chương trình tuần tự tạo và xóa một task mà kích thước stack cho task này không thay đổi.

Selection_016

Hình trên diễn tả qúa trình task được khởi tạo, xóa, và sử dụng lại. A là lúc 3 task đang hoạt động. Heap trông nằm ở trên cùng. B là lúc task 2 được free, và có một lỗ trống nằm giữa task 1 và 3. Tuy nhiên, heap_2 không có cơ chế gom bộ nhớ, nên free space giữa hai task vẫn nằm vậy.

C diễn tả qúa trình một task mới được khởi tạo, hai vùng nhớ TCB và stack được gọi tương ứng; task này đặt vừa vào vùng nhớ giữa. Qúa trình này diễn tả trường hợp hoạt động tốt nhất của heap_2.

Heap_2 sử dụng best fit algorithm cho pvPortMalloc. Ví dụ có 3 khoảng trống trong heap với kích thước 5 Bytes, 25 Bytes, và 50 Bytes. pvPortMalloc được gọi với yêu cầu 20B. Lúc này heap_2 chia vùng nhớ 25B làm 2: 20B và 5B, rồi sử dụng 20B này. Để lại 5B cho lần gọi kế tiếp.

Bạn sẽ không muốn dùng heap_2 cho các ứng dụng pvPortMalloc/pvPortFree được gọi tùy ý.

Cũng như heap_1, configTOTAL_HEAP_SIZE trong FreeRTOSConfig.h khai báo vùng nhớ heap tổng cộng. Tổng các vùng nhớ trống được trả về qua hàm xPortGetFreeHeapSize.

 

Heap_3

Heap_3 sử dụng malloc/free của trình biên dịch cho pvPortMalloc/pvPortFree tương ứng. Heap_3 cung cấp thêm cơ chế bảo vệ hai hàm này để chúng thread safe bằng cách dừng task scheduler.

Nếu dùng gcc, bạn sẽ dùng implementation của newlib.

Với heap_3, linker heap và FreeRTOS là một, và configTOTAL_HEAP_SIZE không có tác dụng gì.

 

Heap_4

Heap_4 được xem như là phiên bản cải tiến của heap_2, với khả năng gom các vùng nhớ trống liên tiếp nhau thanh một block lớn hơn.

Ví dụ bạn có var_1, var_2, và var_3 chiếm các vị trí liên tiếp nhau 5, 25, và 100 bytes trong vùng nhớ heap của FreeRTOS. var_1 và var_2 được xóa đi, heap_2 sẽ tạo ra hai vùng nhớ trống 5 và 25 bytes; heap_4 gom lại rồi tạo ra vùng nhớ trống 30 bytes.

Heap_4 nên được sử dụng khi chương trình liên tục tạo/xóa task, queue, semaphore …, vùng heap của hệ thống sẽ ít bị phân mảnh hơn, so với heap_2.

FreeRTOS đồng thời tuyên bố nó tốt hơn hầu hết các malloc implementation khác? (xem http://www.freertos.org/a00111.html).

 

Bình luận

sử dụng heap_*.c nào tùy thuộc vào app của bạn. Đơn gianr như heap_1, hoặc heap_4 với đánh đổi tài nguyên của hệ thóng.

 

Xác nhận: Các hình ở trên được lấy từ sách Using The FreeRTOS Realtime Kernel của Richard Barry.

Học FreeRTOS, p1

 

Tôi bắt đầu series này với tư cách là người hoàn toàn mới về RTOS nói chung và FreeRTOS nói riêng. Tôi sẽ liệt kê các bình luận và code khi đọc các chương của cuốn Using The FreeRTOS Realtime Kernel.

Tại sao FreeRTOS? Sau một thời gian làm trong lĩnh vực hệ thống nhúng, và IOT, tôi nghĩ nếu một phần cứng không chạy được Linux nhúng, nó nên chạy với FreeRTOS và lwIP? Bạn có thể đưa luận điểm có những nơi mà FreeRTOS+lwIP không phù hợp, Contiki hay RIOT phù hợp hơn. Nhưng qủa thật khả năng kết nối trực tiếp và bảo mật cao từ node đến cloud mang lại sự tiện lợi vô cùng lớn so với việc sử dụng border router.

Do tôi không muốn phụ thuộc vào phần cứng khi làm quen với RTOS này, nên đã setup một môi trường mô phỏng trên Linux, ở https://github.com/nqd/freertos_linux_devl. Trong môi trường này v8.2.3 được sử dụng, lwIP v1.4.1 về sau cũng được tích hợp vào.

Môi trường này không tồn tại nếu không có bản port tuyệt vời của William Davy và fork của megakilo.

Những dòng lệnh sau sẽ chuẩn bị môi trường cho phần này và những phần sau.


git clone --recursive https://github.com/nqd/learn_freertos.git

cd freertos_linux_devl

make -f Makefile.tools

Nếu bạn tò mò, Makefile.tools để lấy freertos v8.2.3 xuống.

Bài đầu tiên, tạo task.


cd ../chapter1

make

./create_task.out

Bình luận

Nếu thay phần vTaskDelay(1000 / portTICK_RATE_MS); bằng vòng lặp for.

Với For delay

Cho mainDELAY_LOOP_COUNT gía trị 10000, kế qủa là task 1 được in ra liên tiếp, rồi đến task 2 và ngược lại.


Task 1 is running
Task 1 is running
...
Task 1 is running
Task 1 is running
Task 1 is running
Task 2 is running
Task 2 is running
...
Task 2 is running

Sau đó tăng mainDELAY_LOOP_COUNT lên 1000000 (tùy thuộc vào tốc độ của PC đang chạy chương trình), kết qủa Task 1/2 được in ra luân phiên nhau.

Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 2 is running
Task 1 is running

...

Nguyên nhân? Xem hình dưới. Mỗi task chạy một khoảng delta(T) = t2-t1, ứng với thời gian giữa các tick của kernel. mainDELAY_LOOP_COUNT càng nhỏ thì càng nhiều vòng lặp được thực hiện trong khoảng delta(T) này, dẫn đến Task x is running xuất hiện liên tục nhiều hơn.

freertos-task1-task2-same-priority-delay

Nếu cho task 2 có priority cao hơn, ví dụ bằng cách #define mainTASK2_PRIORITY (tskIDLE_PRIORITY + 2). Lúc này task 1 không có cơ hội được thực thi.


Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running

freertos-task-priorities

Việc delay bằng vòng lặp for dẫn đến các task có mức độ ưu tiên thấp hơn không có điều khiện thực thi. Đều này nên tránh khi viết chương trình.

vTaskDelay, không For

Quay trở lại với delay bằng vTaskDelay. Chương trình cho kết qủa


Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running

một cách tuần tự dù Task 2 có độ ưu tiên cao hơn Task 1. Vì sao?

Lúc này xuất hiện khái niệm Blocked/Ready state.

freertos-tasks

Mỗi task nằm trong một trong các trạng thái

  • Blocked là một tráng thái “Not Running”. Lúc này task đang chờ một trong hai sự kiện: Time hoặc Synchronization. Liên quan đến thời gian như Task X sẽ chuyển đến trạng thái Block trong 100 ms. Synchronization khi nó đang tương tác với các Task khác (semaphore, queue, …) hoặc interrupt.
  • Suspended là một trạng thái “Not Running”. Một Task vào trạng thái này khi được gọi với vTaskSuspend(), và thoát ra với vTaskResume() hoặc xTaskResumeFromISR(). Task với trạng thái này sẽ được được schedule bởi kernel.
  • Ready cũng là một trạng thái “Not Running”. Task này sẵng sàng để chạy, nhưng chưa chuyển sang Running.
  • Running là trạng thái Task được CPU thực thi.

Quay trở lại chương trình, Task 2 với ưu tiên cao hơn, được thực thi trước. Khi nó được chuyển sang trạng thái Blocked với hàm vTaskDelay, Task 1 được kernel gọi thực thi. Sau đó Task 1 cũng được chuyển sang trạng thái Blocked này.

Kernel sang trạng thái idle, và vApplicationIdleHook được gọi. Chương trình có thể đặt cpu sang chế độ ngủ nếu năng lượng là vấn đề của hệ thống.

Sau khoảng thời gian delay, Task 2 chuyển sang trạng thái Running, in ra terminal, rồi lại chuyển sang Blocked. Tương tự như vậy với Task 1.

Xác nhận: Các hình ở trên được lấy từ sách Using The FreeRTOS Realtime Kernel của Richard Barry.