Trời tính không bằng máy tính

blog, dev log, Hacking

#40: Tôi đã code lịch âm trên C như thế nào?

Đã khá lâu rồi mình mới viết blog trở lại. Một phần do mình bị mất cảm hứng, mà một phần lớn cũng là do mình tạm nhường sân chơi cuộc đời cho art.ngxson.com

Tại sao?

Vào chủ đề chính, tại sao mình phải tạo ra một app lịch âm-dương, mà tại sao lại bắt buộc code trên C nhỉ? Vậy thì mình sẽ giới thiệu một câu chuyện sâu xa, đó là chiếc smartwatch mình đang đeo: chiếc Amazfit Bip

Chiếc đồng hồ đã theo mình tới giờ là 3 năm rồi, bằng số thời gian mình sống trên đất Pháp. Thông số của nó cũng chằng có mấy: CPU 80MHz, RAM 128KB và 8MB bộ nhớ flash. Dù chỉ có vài tính năng cơ bản như đo nhịp tim, chạy bộ, báo thức,… nhưng điểm đặc biết nhất của chiếc đồng hồ này đó chính là cộng đồng hack / reversed engineeringchủ yếu là mấy anh Nga ngố. Maxim Volkov là người đã tạo ra BipOS – một phiên bản hệ điều hành tùy chỉnh. Điều này mở ra khả năng tự lập trình, cài app vào chiếc đồng hồ này.

Tất nhiên, do hạn chế về tốc độ xử lý cũng như bộ nhớ, tất cả app phải được code trên C.

Ý tưởng

Đầu tiên, mình cần vạch ra các yêu cầu đặc biệt:

  • Code phải tối ưu để tránh xử lý nhiều khi chạy
  • File binary (đã compile) phải nhẹ (dưới 20KB là hợp lý)
  • Hiện được lịch âm và lịch dương. Do màn hình đồng hồ nhỏ nên chỉ cần hiện ngày tháng âm đầy đủ của 1 ngày đầu tiên của tháng.
  • Như vậy, cần xử lý tới tháng nhuận trong âm lịch (tháng có 19 ngày), nhưng không cần để ý tới năm nhuận âm lịch (năm có 13 tháng)
  • Có thể viết code nhanh (trong 1 ngày thì tốt), vì budget thời gian của mình có hạn.
Ý tưởng ban đầu

Như vậy, mình sẽ cần tìm hiểu 2 phần:

  1. Nguồn dữ liệu: dữ liệu về các ngày trong tháng sẽ lấy từ đâu (ví dụ 01/02/2021 dương sẽ vào thứ 2, trùng với 20/12/2020 âm)
  2. Hiển thị => dễ làm nếu chỉ printf ra console, nhưng mình phải chạy nó lên smartwatch, tức là phải có code quản lý tọa độ chữ, số nữa.

Nguồn dữ liệu

Ý tưởng đầu tiên của mình là sẽ viết lại thuật toán tính thứ trong tuần, rồi thuật toán chuyển ngày âm-dương, có thể sẽ viết lại từ code python gốc, hoặc nếu có lib C có sẵn thì càng tốt.

Tuy nhiên, sau khi xem xong code tại quangvinh86/SolarLunarCalendar, mình nhận thấy nó có 2 vấn đề: 1 là thú thực code rồi tính toán nhiều quá, mình không hiểu. Mà 2 là nó dùng đến sin, cos, tức là sẽ tốn kha khá thời gian xử lý trên cái CPU 80MHz của Amazfit Bip kia.

Thế là mình đến với ý tưởng 2: làm cách nào đó “chụp” lại dữ liệu của lịch có sẵn, rồi lưu cứng vào code (hardcode). Mình định sẽ “chụp” từ app lịch viết trên JS của anh Ho Ngoc Duc

Cách này giải quyết được vấn đề về xử lý, nhưng lại đặt ra một vấn đề khác: làm thế nào để sử dụng bộ nhớ hiệu quả? Giả sử mỗi tháng có 30 ngày đi, mỗi ngày chiếm 1 byte cho ngày âm và 1 byte cho ngày dương. Như vậy, một tháng chiếm 60 bytes, một năm chiếm 720 bytes. Thế thì 20KB chỉ đủ lưu có khoảng 28 năm mà thôi!

Ý tưởng thứ 3: “nén” dữ liệu:

Quan sát kỹ hơn, mình nhận thấy rằng mỗi tháng được định nghĩa bởi các thông số sau:

  1. mùng 1 dương của tháng đó là thứ mấy? => tính thì phức tạp, nên là sẽ lưu. Dữ liệu là số từ 0 đến 6; 0 = CN và 6 = T7.
  2. mùng 1 dương của tháng đó trùng với ngày nào, tháng nào, năm nào trong âm lịch => cũng sẽ lưu. ngày là số từ 1 ≤ d ≤ 30, tháng 1 ≤ m ≤ 12, năm thì hiện tại cứ cho là chỉ lưu 2 số cuối của năm đi: 0 ≤ y ≤ 99
  3. Tháng đó có bao nhiêu ngày (dương) => cái này có quy tắc, sẽ code vào
  4. Tháng âm đó có nhuận không => cũng sẽ lưu, Dữ liệu là boolean (0 hoặc 1)

Như vậy nhẩm tính, cần phải lưu 5 integers tất cả, cái nào cũng nhỏ hơn 255 nên có thể lưu vào 1 byte (hay còn gọi là char trong c). Tuy nhiên, mình có thể làm tốt hơn chứ:

Ý tưởng căn bản là sẽ dùng chung 1 byte cho 2 số liệu (nếu có thể). Ví dụ, thứ và tháng đều là số nguyên < 2^4, nên có thể dùng 4 bits cho thứ và 4 bits cho tháng. Mình chỉ sử dụng có 3 bytes để lưu cả 5 số liệu này, tất nhiên có thể làm tốt hơn nữa cơ, nhưng mình lười…

Bắt tay vào làm thôi

Code tham khảo: prototype.html

  • Đầu tiên, mình sử dụng code của anh Ho Duc Ngoc để in lịch ra màn hình
  • Sau đó, query div chứa ngày đầu tiên của tháng, đọc data của ngày đó
  • Tính toán 3 bytes dữ liệu cho tháng đó
  • Lặp lại, mình làm với mọi tháng từ 1/1980 tới 12/2059, vậy là khoảng 80 năm, 960 tháng => khoảng 2.8KB

Về phần hiển thị, mình sẽ không giải thích quá nhiều vì logic thực ra cũng khá đơn giản: mình tạo một hàm kiểu “printf” của riêng mình, nó nhớ tọa độ của dòng trước, character trước đã được in ra. Các tọa độ như lề, kích cỡ con chữ,… đều được hardcoded:

Thành quả

Link code trên github: ngxson/hobby_amazfit_bip_am_lich

App này là thành quả của 1 ngày trời lập trình, tối ưu và fix lỗi. App có thể hiện lịch từ năm 1980 đến 2060. Phần vui nhất khi mình dev con app này đó là ngồi tối ưu code: Toàn bộ data và code được tối ưu để nhét vừa vào khoảng 10KB.

Nui Nguyễn

Cảm ơn các bạn!

MỚI! Đăng ký nhận bài viết mới nhất qua chatbot: