[ESP32 Timer] Điều khiển nhiều tác vụ thông qua timer

Mở đầu

Chào các bạn, chuyện là lần nào vào mở bài mình đều nói lịch sử lúc trước mình làm những thứ liên quan nên hôm nay mình vẫn nói :D. Cách đây 5 năm khi mình làm luận văn với đề tài robot vẽ, loại robot được sử dụng là Scara. Hồi đó làm luận văn đều tự mình làm tất tần tật từ cơ khí, mạch điện, chương trình C# xử lý điểm ảnh, arduino điều khiển động cơ đều phải tự viết không thông qua thư viện hay phần mềm nào khác như GRBL gì cả. Nên dù sao cũng được đánh giá cao là tự làm cộng thêm một số giải thuật làm cho xử lý sai số góc của cánh tay Scara (do động cơ không mượt và cánh tay đòn dài nên sai số sẽ lớn) => 7.5 điểm về chỗ :v. Nhưng sau vài năm đi làm nhìn lại con robot mình vẽ thì thấy nó hơi tù tù, hình vẽ ra có dạng vuông vuông, nhiều khi có vệt tròn. Ngẫm nghĩ lại thì do mình làm chưa hoàn hảo vụ điều khiển 2 động cơ bước tuy khác nhau về hành trình nhưng cùng di chuyển và kết thúc đồng thời được. Điều này cũng làm mình chưa thể làm một số máy CNC có đặc thù riêng biệt, bởi vậy mình cứ làm mấy cái máy tù tù đơn giản thôi. Nay mình thử sử dụng timer để điều khiển xem sao, cũng đã build được một cái máy CNC tù tù đơn giản với chức năng đục lỗ. Dưới đây là sản phẩm của máy với lỗ kích thước 0.3mm, khoảng cách giữa hai lỗ là 0.5mm :D.

Vậy nên hôm nay mình giới thiệu các bạn tổng quát về timer trên ESP32. Lâu nay dùng esp8266 riết nên giờ đổi sang ESP32 cho có bài đăng :D.

Video mình tham khảo:

Về kiến thức timer thì mình sẽ nêu những ý chính và lưu ý khi sử dụng từ video này. Bao gồm:

Khai báo và cài đặt timer

Trong hàm Setup mình sẽ thiết lập timer0 với setting: “timer0 = timerBegin(0, 80, true);”. Có thể hiểu nôm na là: khai báo timer0, mỗi chu kỳ clock là 12.5ns, nên khi thiết lập 80 sẽ là 80 * 12.5ns = 1000ns = 1us bước đếm tăng dần.

Sau đó gán vào hàm cần sử dụng: “timerAttachInterrupt(timer0, &onTimer0, true);”. Ở đây mình sử dụng hàm onTimer0.

[code]
void IRAM_ATTR onTimer0() {
portENTER_CRITICAL_ISR(&timerMux0);
//do something here
portEXIT_CRITICAL_ISR(&timerMux0);
}

[/code]

Ở đây cần lưu ý rằng code của bạn trong hàm onTimer0 bắt buộc nằm giữa “portENTER_CRITICAL_ISR(&timerMux0);” và “portEXIT_CRITICAL_ISR(&timerMux0);” khi sử dụng từ 2 timer trở lên.

Trong video tại hàm loop sẽ có dòng “vTaskDelay(portMAX_DELAY);” các bạn nên comment nó nếu muốn xử lý tác vụ khác nhé.

Cách sử dụng timer

Mình sẽ ngắt để vào hàm timer bằng cách setting như sau: “timerAlarmWrite(timer0, 1000000, true);”. Nghĩa là sẽ gọi hàm onTimer0 sau mỗi chu kỳ 1000000us = 1s.

Sau đó sử dụng “timerAlarmEnable(timer0);” để bắt đầu chạy hàm onTimer0 sau mỗi 1s.

Các bạn có thể dừng chạy hàm timer bằng cách “timerAlarmDisable(timer0);”

Điều khiển nhiều tác vụ khác nhau về hành trình nhưng khởi động đồng thời và kết thúc đồng thời

Mình làm việc này với mục đích đồng bộ nhiều động cơ Step. Giả sử một máy CNC muốn di chuyển từ tọa độ (0; 0) đến tọa độ (5; 5). Nếu các bạn code theo kiểu phân biệt vòng for cho mỗi động cơ bước như thế này:

[code]
for(int x = 0; x < 200; x++) { digitalWrite(stepPin0,HIGH); delayMicroseconds(500); digitalWrite(stepPin0,LOW); delayMicroseconds(500); } for(int x = 0; x < 400; x++) { digitalWrite(stepPin1,HIGH); delayMicroseconds(500); digitalWrite(stepPin1,LOW); delayMicroseconds(500); } [/code]

Thì máy CNC sẽ di chuyển theo hai đường vuông góc: từ (0; 0) đến (0; 5) sau đó từ (0; 5) đến (5; 5). Điều này sẽ làm cho đường đi của CNC sẽ không được mượt.

Đó là vấn đề đặt ra, vì vậy cách giải quyết chỉ có thể là dùng nhiều timer cho động cơ Step để loại bỏ hoàn toàn hàm delay mà ta thường viết.

Vậy giả sử ta có 3 động cơ bước cần hoạt động đồng thời, khác số bước cần di chuyển và muốn nó kết thúc chu trình đồng thời, ta sẽ dùng 3 bộ timer0, timer1, timer2.

Việc đầu tiên cần làm là xác định số bước của động cơ nào lớn nhất trong 3 động cơ bước. Sau khi xác định được rồi ta sẽ lấy timer của động cơ bước này làm chuẩn. Ví dụ Step0 = 200, Step1 = 150, Step2 = 49 thì Step0 sẽ là lớn nhất, ta lấy timer0 làm chuẩn. Việc lấy timer làm chuẩn dựa vào tốc độ mình mong muốn của máy CNC di chuyển. Ví dụ mỗi động cơ bước sẽ di chuyển theo “delayMicroseconds(500);” thì ta có thể setting cho nó: ” “timerAlarmWrite(timer0, 500, true);”. Nghĩa là cứ sau 500us thì cứ đổi trạng thái chân stepPin0 là được, và khi so sánh với số bước đủ 500 thì sẽ ngừng chạy timer0.

Bước tiếp theo là tính chu kỳ chạy cho hai động cơ bước còn lại, ví dụ Step1= 150 thì ta setting cho nó “timerAlarmWrite(timer0, 500 * Step0 / Step1, true);”. Nghĩa là chu kỳ của Step1 sẽ chạy lâu hơn so với Step0 vì hành trình nó ít hơn ấy mà :D.

Sau đó tiến hành sử dụng đồng thời “timerAlarmEnable(timer0);” và “timerAlarmEnable(timer1);” để chạy thì mình nhận thấy rằng, khi chạy hàm này thì hàm onTimer1 và onTimer0 đều chạy đồng thời nhưng không kết thúc đồng thời. Nguyên nhân là ta cần xác định delay giữa hai hàm này nữa các bạn. Nghĩa là timer0 chạy trước một vài bước sau đó timer1 mới bắt đầu chạy (trường hợp cho Step0 là lớn nhất). Vì vậy mình sử dụng thêm điều kiện:

[code]
if (step_up[0] >= step_count[0] / step_count[1] && run_timer1 == false)
{
timerAlarmWrite(timer1, 10000 * step_count[0] / step_count[1], true);
timerAlarmEnable(timer1);
run_timer1 = true;
}
[/code]

Mục đích delay theo tỉ lệ số bước để timer1 chạy sau timer0.

Nói thì dài dòng, mình sẽ chia sẻ code để các bạn có thể dễ hình dung cách thức hoạt động của nó. Lưu ý code này chỉ là code bộ đếm Step theo 3 timer, chứ chưa phải là code để điều khiển động cơ Step đồng bộ nha. Nếu muốn làm, các bạn có thể thêm một chút xíu nữa là có thể thực hiện được.

[code]

include

hw_timer_t * timer0 = NULL;
hw_timer_t * timer1 = NULL;
hw_timer_t * timer2 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;
portMUX_TYPE timerMux1 = portMUX_INITIALIZER_UNLOCKED;
portMUX_TYPE timerMux2 = portMUX_INITIALIZER_UNLOCKED;
bool run_timer = false;
bool run_timer2 = false;
bool run_timer1 = false;
bool run_timer0 = false;
int maxIndex = 0;
Button button1(0);
int step_up[3] = {1, 1, 1};
int step_count[3] = {0, 0, 0};
void IRAM_ATTR onTimer0() {
portENTER_CRITICAL_ISR(&timerMux0);
if (maxIndex == 0)
{
if (step_up[0] >= step_count[0] / step_count[1] && run_timer1 == false)
{
timerAlarmWrite(timer1, 10000 * step_count[0] / step_count[1], true);
timerAlarmEnable(timer1);
run_timer1 = true;
}
if (step_up[0] >= step_count[0] / step_count[2] && run_timer2 == false)
{
timerAlarmWrite(timer2, 10000 * step_count[0] / step_count[2], true);
timerAlarmEnable(timer2);
run_timer2 = true;
}
}
if (step_up[0] > step_count[0])
{
run_timer1 = false;
run_timer2 = false;
timerAlarmDisable(timer0);
}
else
{
Serial.println(String(0) + ” ” + step_up[0]);
step_up[0] += 1;
}
portEXIT_CRITICAL_ISR(&timerMux0);
}
void IRAM_ATTR onTimer1() {
portENTER_CRITICAL_ISR(&timerMux1);
//digitalWrite(2, !digitalRead(2));
if (maxIndex == 1)
{
if (step_up[1] >= step_count[1] / step_count[0] && run_timer0 == false)
{
timerAlarmWrite(timer0, 10000 * step_count[1] / step_count[0], true);
timerAlarmEnable(timer0);
run_timer0 = true;
}
if (step_up[1] >= step_count[1] / step_count[2] && run_timer2 == false)
{
timerAlarmWrite(timer2, 10000 * step_count[1] / step_count[2], true);
timerAlarmEnable(timer2);
run_timer2 = true;
}
}
if (step_up[1] > step_count[1])
{
run_timer0 = false;
run_timer2 = false;
timerAlarmDisable(timer1);
}
else
{
Serial.println(String(1) + ” ” + step_up[1]);
step_up[1] += 1;
}
portEXIT_CRITICAL_ISR(&timerMux1);
}
void IRAM_ATTR onTimer2() {
portENTER_CRITICAL_ISR(&timerMux2);
//digitalWrite(2, !digitalRead(2));
if (maxIndex == 2)
{
if (step_up[2] >= step_count[2] / step_count[0] && run_timer0 == false)
{
timerAlarmWrite(timer0, 10000 * step_count[2] / step_count[0], true);
timerAlarmEnable(timer0);
run_timer0 = true;
}
if (step_up[2] >= step_count[2] / step_count[1] && run_timer1 == false)
{
timerAlarmWrite(timer1, 10000 * step_count[2] / step_count[1], true);
timerAlarmEnable(timer1);
run_timer1 = true;
}
}
if (step_up[2] > step_count[2])
{
run_timer0 = false;
run_timer1 = false;
timerAlarmDisable(timer2);
}
else
{
Serial.println(String(2) + ” ” + step_up[2]);
step_up[2] += 1;
}
portEXIT_CRITICAL_ISR(&timerMux2);
}
void setup() {
Serial.begin(115200);
button1.begin();
Serial.println(“start timer “);
pinMode(0, INPUT_PULLUP);
pinMode(2, OUTPUT);
timer0 = timerBegin(0, 80, true); // timer 0, MWDT clock period = 12.5 ns * TIMGn_Tx_WDT_CLK_PRESCALE -> 12.5 ns * 80 -> 1000 ns = 1 us, countUp
timerAttachInterrupt(timer0, &onTimer0, true); // edge (not level) triggered
timer1 = timerBegin(1, 80, true); // timer 0, MWDT clock period = 12.5 ns * TIMGn_Tx_WDT_CLK_PRESCALE -> 12.5 ns * 80 -> 1000 ns = 1 us, countUp
timerAttachInterrupt(timer1, &onTimer1, true); // edge (not level) triggered
timer2 = timerBegin(2, 80, true); // timer 0, MWDT clock period = 12.5 ns * TIMGn_Tx_WDT_CLK_PRESCALE -> 12.5 ns * 80 -> 1000 ns = 1 us, countUp
timerAttachInterrupt(timer2, &onTimer2, true); // edge (not level) triggered
}

void loop() {
// nope nothing here
if (button1.released())
{
step_up[0] = 1;
step_count[0] = 399;
step_up[1] = 1;
step_count[1] = 213;
step_up[2] = 1;
step_count[2] = 379;
run_timer = true;
}
if (run_timer == true)
{
run_timer = false;
int maxValue = step_count[maxIndex];
for (int i = 1; i < 3; i++) { if (step_count[i] > maxValue) {
maxValue = step_count[i];
maxIndex = i;
}
}
if (maxIndex == 0)
{
timerAlarmWrite(timer0, 10000, true);
timerAlarmEnable(timer0);
}
else if (maxIndex == 1)
{
timerAlarmWrite(timer1, 10000, true);
timerAlarmEnable(timer1);
}
else if (maxIndex == 2)
{
timerAlarmWrite(timer2, 10000, true);
timerAlarmEnable(timer2);
}
}
//vTaskDelay(portMAX_DELAY); // wait as much as posible …
}
[/code]

Code này mình có sử dụng thư viện Button, các bạn download để sử dụng. Mục đích là khi bấm nút Flash ứng với GPIO0 trên board ESP32 thì sẽ bắt đầu chạy đồng bộ 3 bộ đếm. Và đây là kết quả:

Kết quả Step0 = 12, Step1 = 7, Step2 = 23 (cho số lẻ tí xem nó đếm được không :D)

Kết quả hình bên có vẻ chênh nhau một tí xíu, chắc để mình test lại thực tế thử. Cột đầu tiên là timer0, timer1, timer2 nha :D.

Kết quả Step0 = 123, Step1 = 512, Step2 = 657

Kết quả bên này có vẻ ổn hơn này 😀

Kết luận

Với đề tài sử dụng timer này các bạn có thể điều khiển nhiều tác vụ đồng thời. Và cái mình suy nghĩ đầu tiên đó là đồng bộ động cơ bước. Dù sao thì thư viện cũng có đủ cả rồi nhưng tự viết sẽ biết thêm nhiều thứ hay ho hơn :D. Các bạn có ý tưởng về sử dụng timer có thể góp ý cho mình nhé.

Hiện tại code mình viết chưa được hay cho lắm, các bạn có thể góp ý để mình sửa cho phù hợp nha. Bài viết tới nếu mình có thời gian sẽ làm thêm bài giao tiếp UART JSON. Để truyền data từ PC về ESP32 thực hiện tác vụ (cụ thể là tọa độ để chạy động cơ bước dùng timer này)

Mọi chi tiết thắc mắc hãy liên hệ mình

Nguyễn Hữu Phước

Zalo: 0905021462

 741 total views,  9 views today

Leave a Reply

Your email address will not be published. Required fields are marked *