Bài 50.128 phút đọc
Bộ điều khiển LED và máy trạng thái
Sau bài này bạn sẽ làm được
- Thay mọi hàm delay chặn bằng kiểm mốc thời gian
- Dựng tick một mili giây bằng SysTick
- Cài máy trạng thái chuyển chế độ bằng nút bấm
- Điều khiển độ sáng bằng PWM phần mềm và phần cứng
Dự án đầu tiên, và nó dạy kiến trúc quan trọng nhất của lập trình nhúng: một vòng lặp chính không bao giờ chặn, mọi thứ tính theo mốc thời gian, và trạng thái nằm trong dữ liệu chứ không nằm trong luồng thực thi.
#Bỏ mọi hàm delay chặn
Hàm trễ chặn
int main(void)
{
khoi_tao();
for (;;) {
gpio_bat(GPIOA, 5);
tre_ms(500); /* CHAN 500 mili giay */
gpio_tat(GPIOA, 5);
tre_ms(500); /* CHAN them 500 nua */
}
}
/* Trong mot giay do:
- Nut bam khong duoc doc
- UART khong duoc xu ly
- Man hinh khong duoc cap nhat
- Khong lam duoc bat ky viec gi khac
Va them mot den thu hai voi chu ky khac thi khong the. */So mốc thời gian
int main(void)
{
uint32_t moc_den = 0u;
uint8_t trang_thai = 0u;
khoi_tao();
for (;;) {
uint32_t nay = tick_ms();
if ((nay - moc_den) >= 500u) {
moc_den = nay;
trang_thai = !trang_thai;
gpio_ghi(GPIOA, 5, trang_thai);
}
/* Vong lap quay lai ngay, lam duoc moi viec khac */
doc_nut(nay);
xu_ly_uart();
cap_nhat_man_hinh(nay);
}
}Vòng lặp chính không chặn
Mỗi lần lặp, chương trình kiểm mọi việc xem có đến hạn không, làm phần nào đến hạn, rồi quay lại ngay. Không có chỗ nào đứng chờ. Nhờ vậy độ trễ đáp ứng bằng đúng thời gian một vòng lặp.
#Tick một mili giây
systick.c
#include <stdint.h>
#include "stm32f4.h"
static volatile uint32_t g_tick = 0u;
void systick_khoi_tao(uint32_t fck)
{
SysTick->LOAD = (fck / 1000u) - 1u; /* đếm xuống trong 1 ms */
SysTick->VAL = 0u;
SysTick->CTRL = (1u << 2) /* dùng clock lõi */
| (1u << 1) /* cho phép ngắt */
| (1u << 0); /* bật bộ đếm */
NVIC_SetPriority(SysTick_IRQn, 1u); /* ưu tiên cao, ISR rất ngắn */
}
void SysTick_Handler(void)
{
++g_tick; /* việc duy nhất */
}
uint32_t tick_ms(void)
{
return g_tick;
}
/* Chờ có giới hạn, dùng khi thật sự phải chặn */
void tre_ms(uint32_t ms)
{
uint32_t bat_dau = g_tick;
while ((g_tick - bat_dau) < ms) {
__WFI(); /* ngủ giữa hai tick, tiết kiệm điện */
}
}| Nguồn thời gian | Độ phân giải | Quấn vòng sau | Dùng cho |
|---|---|---|---|
| Bộ đếm tick 32 bit, 1 ms | 1 ms | 49.7 ngày | Hầu hết việc |
| SysTick VAL | 1 chu kỳ | 1 ms | Đo trong một mili giây |
| DWT CYCCNT | 1 chu kỳ | 51 giây ở 84 MHz | Đo thời gian hàm |
| Timer 32 bit riêng | Tùy cấu hình | Tùy cấu hình | Đo khoảng dài với độ phân giải cao |
#Máy trạng thái điều khiển chế độ
che-do.h
#ifndef CHE_DO_H
#define CHE_DO_H
#include <stdint.h>
typedef enum {
CD_TAT = 0,
CD_NHAY,
CD_CHAY_DUOI,
CD_THO,
CD_SO_LUONG /* luôn là phần tử cuối, xem Bài 16.1 */
} CheDo;
void che_do_dat(CheDo cd, uint32_t nay);
void che_do_cap_nhat(uint32_t nay);
CheDo che_do_hien_tai(void);
#endifche-do.c
#include "che-do.h"
#include "gpio.h"
#include "pwm.h"
#define SO_DEN 8u
static CheDo s_cd = CD_TAT;
static uint32_t s_moc = 0u;
static uint8_t s_buoc = 0u;
/* Mỗi chế độ một hàm, bảng tra bằng con trỏ hàm, xem Bài 16.2 */
typedef void (*HamCheDo)(uint32_t nay);
static void cd_tat(uint32_t nay)
{
(void)nay;
for (uint8_t i = 0; i < SO_DEN; ++i) den_dat(i, 0u);
}
static void cd_nhay(uint32_t nay)
{
if ((nay - s_moc) < 500u) return;
s_moc = nay;
s_buoc = !s_buoc;
for (uint8_t i = 0; i < SO_DEN; ++i)
den_dat(i, s_buoc ? 255u : 0u);
}
static void cd_chay_duoi(uint32_t nay)
{
if ((nay - s_moc) < 100u) return;
s_moc = nay;
s_buoc = (uint8_t)((s_buoc + 1u) % SO_DEN);
for (uint8_t i = 0; i < SO_DEN; ++i)
den_dat(i, (i == s_buoc) ? 255u : 0u);
}
static void cd_tho(uint32_t nay)
{
/* Chu kỳ 2 giây, sáng dần rồi tối dần, dùng số nguyên */
uint32_t pha = (nay - s_moc) % 2000u;
uint32_t muc = (pha < 1000u) ? pha : (2000u - pha); /* 0 tới 1000 */
uint8_t v = (uint8_t)(muc * 255u / 1000u);
for (uint8_t i = 0; i < SO_DEN; ++i) den_dat(i, v);
}
static const HamCheDo BANG[CD_SO_LUONG] = {
[CD_TAT] = cd_tat,
[CD_NHAY] = cd_nhay,
[CD_CHAY_DUOI] = cd_chay_duoi,
[CD_THO] = cd_tho,
};
_Static_assert(sizeof BANG / sizeof BANG[0] == CD_SO_LUONG,
"bang che do khong khop so luong");
void che_do_dat(CheDo cd, uint32_t nay)
{
if (cd >= CD_SO_LUONG) return;
s_cd = cd;
s_moc = nay;
s_buoc = 0u;
}
void che_do_cap_nhat(uint32_t nay)
{
if (BANG[s_cd] != NULL) BANG[s_cd](nay);
}
CheDo che_do_hien_tai(void) { return s_cd; }main-chuyen-che-do.c
#include "che-do.h"
#include "debounce.h"
#include "systick.h"
int main(void)
{
Nut nut;
systick_khoi_tao(84000000u);
den_khoi_tao();
nut_khoi_tao(&nut, GPIOC, 13u, 0u, 20u);
che_do_dat(CD_NHAY, 0u);
for (;;) {
uint32_t nay = tick_ms();
if (nut_cap_nhat(&nut, nay) && nut_dang_nhan(&nut)) {
CheDo ke = (CheDo)((che_do_hien_tai() + 1u) % CD_SO_LUONG);
che_do_dat(ke, nay);
}
che_do_cap_nhat(nay);
den_lam_moi(nay); /* PWM phần mềm, xem mục sau */
}
}terminal
> theo-doi-che-do
t=0 che do NHAY t=2841 che do CHAY_DUOI t=5102 che do THO t=7488 che do TAT t=9210 che do NHAY
#PWM phần mềm cho nhiều đèn
Chip chỉ có vài kênh PWM phần cứng. Muốn điều khiển độ sáng tám đèn thì phải làm PWM bằng phần mềm, và cách làm nó là một bài học hay về đánh đổi giữa CPU và phần cứng.
pwm-mem.c
#include <stdint.h>
#include "gpio.h"
#define SO_DEN 8u
#define SO_MUC 256u
static const struct { GPIO_TypeDef *cong; uint8_t chan; } DEN[SO_DEN] = {
{ GPIOB, 0u }, { GPIOB, 1u }, { GPIOB, 2u }, { GPIOB, 3u },
{ GPIOB, 4u }, { GPIOB, 5u }, { GPIOB, 6u }, { GPIOB, 7u },
};
static uint8_t s_muc[SO_DEN] = { 0 };
void den_dat(uint8_t i, uint8_t muc)
{
if (i < SO_DEN) s_muc[i] = muc;
}
/* Gọi thật thường xuyên từ vòng lặp chính.
Bộ đếm chạy 0 tới 255, đèn bật khi bộ đếm nhỏ hơn mức của nó. */
void den_lam_moi(uint32_t nay)
{
(void)nay;
static uint8_t dem = 0u;
/* Gom mọi bit của cùng một cổng lại, ghi BSRR MỘT lần */
uint32_t bat = 0u, tat = 0u;
for (uint8_t i = 0; i < SO_DEN; ++i) {
if (s_muc[i] > dem) bat |= (1u << DEN[i].chan);
else tat |= (1u << (DEN[i].chan + 16u));
}
GPIOB->BSRR = bat | tat; /* một lệnh ghi cho cả tám đèn */
++dem; /* uint8_t tự quay về 0 sau 255 */
}pwm-mem-ngat.c
/* Cach chua: goi den_lam_moi tu mot NGAT dinh thoi,
nen tan so co dinh bat ke vong lap chinh dang lam gi. */
void TIM4_IRQHandler(void)
{
TIM4->SR = ~(1u << 0);
den_lam_moi(0u); /* 11 chu ky, du ngan cho ISR */
}
/* Cau hinh TIM4 ngat o 256 * 200 = 51200 Hz
-> tan so PWM = 200 Hz, on dinh tuyet doi.
Tai CPU = 51200 * 11 / 84000000 = 0.67 phan tram. */| Cách | Số đèn | Tải CPU | Tần số ổn định |
|---|---|---|---|
| PWM phần cứng | Bằng số kênh timer, thường 4 tới 8 | 0 | Có |
| PWM phần mềm từ vòng lặp chính | Không giới hạn | 1 tới 5 phần trăm | Không |
| PWM phần mềm từ ngắt | Không giới hạn | 0.7 phần trăm | Có |
| Chip điều khiển đèn ngoài, ví dụ WS2812 | Hàng trăm | Gần 0 nếu dùng DMA | Có |
#Ghép lại thành chương trình hoàn chỉnh
main.c
#include <stdint.h>
#include "che-do.h"
#include "debounce.h"
#include "den.h"
#include "systick.h"
#include "uart.h"
#define FCK 84000000u
static Nut s_nut;
static void khoi_tao_tat_ca(void)
{
systick_khoi_tao(FCK);
uart_khoi_tao(FCK, 115200u);
den_khoi_tao();
tim4_khoi_tao(FCK, 51200u); /* ngắt cho PWM phần mềm */
nut_khoi_tao(&s_nut, GPIOC, 13u, 0u, 20u);
che_do_dat(CD_NHAY, 0u);
uart_gui_chuoi("Bo dieu khien den, san sang\r\n");
}
int main(void)
{
khoi_tao_tat_ca();
uint32_t moc_bao_cao = 0u;
for (;;) {
uint32_t nay = tick_ms();
/* 1. Đọc nút, đã chống rung */
if (nut_cap_nhat(&s_nut, nay) && nut_dang_nhan(&s_nut)) {
CheDo ke = (CheDo)((che_do_hien_tai() + 1u) % CD_SO_LUONG);
che_do_dat(ke, nay);
uart_gui_chuoi("che do ");
uart_in_uint((uint32_t)ke);
uart_gui_chuoi("\r\n");
}
/* 2. Cập nhật hiệu ứng đèn theo chế độ hiện tại */
che_do_cap_nhat(nay);
/* 3. Báo cáo mỗi 5 giây */
if ((nay - moc_bao_cao) >= 5000u) {
moc_bao_cao = nay;
uart_gui_chuoi("chay duoc ");
uart_in_uint(nay / 1000u);
uart_gui_chuoi(" giay\r\n");
}
/* PWM do ngắt TIM4 lo, vòng lặp chính không đụng tới */
}
}terminal
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -Wall -Wextra -Tlinker.ld -nostartfiles startup.c systick.c gpio.c debounce.c den.c che-do.c uart.c main.c -o den.elf
arm-none-eabi-size den.elf
text data bss dec hex filename 3184 12 124 3320 cf8 den.elf
st-flash write den.bin 0x8000000
st-flash 1.7.0 file den.bin md5 checksum: 3f2a1c... Flash written and verified! jolly good!
picocom -b 115200 /dev/ttyUSB0
Bo dieu khien den, san sang chay duoc 5 giay che do 2 chay duoc 10 giay che do 3
Mở rộng tiếp theo
| Mở rộng | Học thêm được gì | Bài liên quan |
|---|---|---|
| Nhấn giữ để chỉnh độ sáng | Sự kiện nhấn giữ, thay đổi liên tục theo thời gian | Bài 47.5 |
| Lưu chế độ vào Flash để giữ sau khi mất điện | Ghi Flash, đọc lại lúc khởi động | Bài 50.4 |
| Điều khiển qua UART thay vì nút | Bộ đệm vòng, tách lệnh | Bài 50.2 |
| Chuyển PWM phần mềm sang phần cứng | Timer, kênh so khớp, DMA | Bài 47.7 |
| Đèn phản ứng theo ánh sáng môi trường | ADC, lọc nhiễu, số học dấu chấm cố định | Bài 50.3 |
Tự làm thử
- Viết chương trình nhấp nháy đèn bằng hàm trễ chặn, rồi viết lại bằng mốc thời gian và thêm một đèn thứ hai chu kỳ khác.
- Cài
systick_khoi_taovà đo số vòng lặp chính mỗi giây, rồi ghi lại chu kỳ lâu nhất. - Cài máy trạng thái bốn chế độ với bảng con trỏ hàm, và xác nhận
_Static_assertbắt được khi bạn thêm chế độ mà quên thêm dòng vào bảng. - Cài PWM phần mềm gọi từ vòng lặp chính, thêm một việc chậm vào vòng lặp, và quan sát đèn nhấp nháy.
- Chuyển PWM phần mềm sang gọi từ ngắt định thời và đo tải CPU bằng bộ đếm vòng lặp nhàn rỗi.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Vòng lặp chính không bao giờ chặn. Mọi hoạt động kéo dài phải thành máy trạng thái với mốc thời gian.
- Trạng thái nằm trong biến, không nằm trong luồng thực thi. Nhờ vậy đọc và sửa được từ bất cứ đâu.
- SysTick cho tick một mili giây với ISR chỉ một dòng, và
uint32_tlà đủ vì phép trừ đúng cả khi quấn vòng. - Bảng con trỏ hàm cho máy trạng thái, cộng
_Static_assertchặn trường hợp quên thêm dòng. - PWM phần mềm gọi từ vòng lặp chính có tần số không ổn định. Gọi từ ngắt định thời thì ổn định và tốn ít CPU hơn.