AK Embedded Base Kit - STM32L151 - Application Startup Code

Startup Code có thể là một từ khóa mới đối với các bạn học lập trình nhúng, vì thông thường các bạn mới bắt đầu hay sử dụng các IDE như Keil, IAR,... để khởi tạo code cho các dự án. 
Khi sử dụng IDE, sau khi chọn mã vi điều khiển (MCU), IDE sẽ tạo tự động Startup Code tự động cấu phù hợp với con MCU mà bạn chọn. Quá trình code thường bắt đầu từ hàm main(), trước khi vào hàm main() vi điều khiển làm những gì các bạn hay bỏ qua.
 

AK Embedded Base Kit - STM32L151 - Application Startup Code

 

Các bạn lưu ý là Startup Code trong các bài ví dụ của mình viết bằng C để các bạn dễ nắm bắt, Startup Code thường là viết bằng ASM. Để nghiên cứu thêm môn này các bạn có thể đọc thêm tài liệu sau: Building_bare-metal_ARM_with_GNU
 

Startup Code có chức năng gì?

  • Một là cấu hình ngoại vi quan trọng để MCU khởi chạy, ví dụ clock chẳn hạn.
  • Hai là thao tác bộ nhớ cho chương trình chính chạy.
 
Để tìm hiểu chức năng thứ 2 của Startup Code chúng ta cần hiểu các biến được lưu như thế nào bên trong vi điều khiển.
 
Ví dụ 1:
#include <stdio.h>
int _val  = 10;
int main() {
printf("%d\n", _val);
}
Kết quả: 10
 
Ví dụ 2:
#include <stdio.h>
int _val ;
int main() {
printf("%d\n",  _val);
}
Kết quả: 0
 
Trong 2 ví dụ trên biến _val sẽ được compiler, và Startup Code xử lý bằng 2 cách khác nhau, để bắt đầu tìm hiểu, các bạn quay lại xíu về cấu trúc máy tính nhé.
 
RAM: 
  • Đọc ghi đều nhanh, khi mất nguồn có lại (Power On Reset) dữ liệu trên RAM sẽ mất hết
  • Các biến khai báo đều được cấp phát trên RAM.
 
FLASH: 
  • Đọc nhanh, ghi thì rất cực (bạn phải xóa sector (256 byte) rồi mới ghi dữ liệu vào sector đã xóa trước đó mới được) khi mất điện có lại thì dữ liệu trên Flash vẫn còn nguyên.
  • Chúng ta chỉ ghi Flash 1 lần khi nạp firmware cho vi điều khiển, trong quá trình chạy hiếm khi chúng ta ghi Flash. (Ghi Flash nội bao lằng nhằng)
Với cấu trúc bộ nhớ trên thì quá trình biên dịch và Startup Code như thế nào để vi điều khiển có thể chạy đúng như lập trình C được.
 
Với góc nhìn bộ nhớ & biên dịch thì có 2 loại biến như sau:
  • 1. Biến có Symbol (thường là các biến không nằm trong bất kì dấu {} nào)
  • 2. Biến Non-Symbol (biến nằm trong {})
 

Biến có Symbol

Có symbol được hiểu là, sau khi biên dịch thành công code, từ .map file,  các bạn có thể biết được biến đó được lưu ở địa chỉ nào, kích thước bộ nhớ của biến là bao nhiêu. Biến có Symbol chia làm 2 loại hoàn toàn khác nhau:

Loại cần khởi tạo giá trị ban đầu ( init data)

Ở ví dụ 1, biến _val được gán giá trị là 10. tức là biến _val cần được khởi tạo giá trị 10 trước khi được sử dụng.
Compiler sẽ gom tất cả các biến loại này nhét chung 1 chổ trên RAM, và nhét chung các giá trị tương ứng với các biến đó vào 1 vùng Flash, vị trí các biến trên RAM và các giá trị trên Flash tương ứng với nhau, để khi Startup Code copy toàn bộ dữ liệu của vùng cần khởi tạo trên Flash lên RAM, thì các biến cần init sẽ được gán đúng giá trị.
  • Vùng dữ liệu - các biến được nhét trên có địa chỉ bắt đầu tại _data (RAM)
  • Vùng dữ liệu - các giá trị được nhét trên có địa chỉ bắt đầu tại _ldata (Flash)
Môn tùy biến vị trí lưu các biến trong chương trình của mình các bạn search từ khóa "Linker Scripts" nhé.
 
Trên source code của AK Embedded Base Kit:
 

Startup code copy init data từ Flash lên RAM

 

Loại không cần khởi tạo giá trị ban đầu (non-init data)

Ở ví dụ 2, biến _val không được gán giá trị, nên giá trị của nó trước khi được sử dụng là 0.
Đối với các biến loại này, compiler gom tất cả các biến lại nhét vào trong section .bss (RAM) sau đó Startup Code clear toàn bộ dữ liệu trong vùng .bss là xong, khá nhàn !
 
Chốt lại để dễ nhớ nhé !
  • Biến có khởi tạo giá trị ban đầu sẽ tốn bộ nhớ trên RAM và FLASH
  • Biến không cần khởi tạo giá trị ban đầu thì chỉ tốn RAM.
Đấy đừng có mà khai báo biến lung tung.
 

Biến không có Symbol (No-Symbol)

Biến không có symbol là các biến được khai báo nằm trong {}, tức là biến khai báo nằm trong một hàm nào đó, các biến này sẽ được nhét vào trong stack pointer. 
Lưu ý: Đối với các biến nằm trong hàm và có từ khóa static thì tương đồng với biến có Symbol nhé.
 
Ví dụ 3:
#include <stdio.h>
void main() {
int _val = 10;
int _val1;
printf("_val: %d\n", _val);
printf("_val1: %d\n", _val1);
}
Kết quả: 
_val: 10
_val1: "đếch biết :)"
 
Trong ví dụ 3, biến _val và _val1 được khai báo nằm trong hàm main(), các biến sẽ trên sẽ được nhét vào trong stack (RAM) và compiler sẽ xử lý (mình nể ông nào chế ra cái cơ chế này), mình giải thích nôn na vầy cho các bạn dễ hình dung:
Mỗi khi vào một hàm compiler sẽ nhớ địa chỉ SP pointer, ví dụ địa chỉ biến &_val sẽ tương đương với [SP + 1] , & _val1 sẽ tương đương với [SP + 2], compiler sẽ chỉ thao tác dựa trên stack pointer SP.
Khi bạn thao tác gián _val = 10, thì sẽ tương đương với *[SP + 1] = 10. Compiler sẽ biên dịch và đảm bảo CPU thực hiện logic như bạn code.
Như vậy, các biến khai báo trên hàm sẽ được đẩy vào stack và code sẽ nhớ vị trí các biến dựa vào stack pointer hiện tại khi bắt đầu vào hàm đó.
Trong các câu hỏi phỏng vấn người ta hay hỏi thường là biến _val1 sẽ in ra giá trị là bao nhiêu, câu trả lời là đếch biết là đúng, vì giá trị tại địa chỉ [SP + 2] là bất định, có thể là 0 nếu stack pointer lần đầu sử dụng nó, có thể là số khác nếu có hàm trước đó sử dụng.
 
Đối với Startup Code, Stack để nguyên, không thao tác không xóa, nếu bạn muốn làm tính năng kiểm tra kích thước stack sử dụng trong quá trình code chạy thì có thể gán toàn bộ stack mang giá trị nào đó giống nhau, kiểu 0xDEADC0DEu chẳn hạn, để MCU chạy một thời gian mình dump stack ra để biết ứng ụng thường ăn bao nhiêu stack, và tối ưu kích thước của stack nhường lại không gian cho các phần khác.
 

Static constructor cho mấy ông ưa sài C++

Khác với C, nếu cần khởi tạo thì chỉ khởi tạo cho biến. Với C++, khởi tạo cho Object thường là 1 hàm, hàm đó được gọi là constructor. Các constructor phải được gọi trước khi sử dụng. Nếu Object được khai báo trong hàm (trong dấu {}) thì khỏe, vào là compiler biên dịch code cho gọi luôn. Trong trường hợp mà khai báo Object không nằm trong bất kì hàm nào, thì các constructor của Object gọi là các static constructor, nó buộc phải được gọi trước khi Object lôi ra sử dụng.
Compiler sẽ gom tất cả các static constructor bỏ vào mảng và Startup Code sẽ gọi hết các hàm đó ra trước khi nhảy vào main().
Các bạn lưu ý cái này nha, nhất là viết thư viện ngoại vi sử dụng C++, tránh xung đột. Không để ý cái này có biến debug code hơi chua.
 
Startup Code của AK Embedded Base Kit: system.c
 

Startup code gọi các static contructors

 
Các bạn đọc xong bài này nghiên cứu thêm các chủ đề với các từ khóa sau: Linker File, Bare-Metal project và Startup Code, nắm được các kiến thức này sẽ giúp cho mình thiết kế chương trình nhúng bay bổng hơn, chế ra nhiều tính năng hay ho hơn.
 

Nếu trong quá trình thao tác có phát sinh các lỗi hoặc các thắc mắc, các bạn thông tin về EPCB theo các kênh sau nhé !