Bỏ qua điều hướng, tới nội dung chính
Học C
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ảiQuấn vòng sauDùng cho
Bộ đếm tick 32 bit, 1 ms1 ms49.7 ngàyHầu hết việc
SysTick VAL1 chu kỳ1 msĐo trong một mili giây
DWT CYCCNT1 chu kỳ51 giây ở 84 MHzĐo thời gian hàm
Timer 32 bit riêngTùy cấu hìnhTù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);

#endif
che-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áchSố đènTải CPUTần số ổn định
PWM phần cứngBằng số kênh timer, thường 4 tới 80Có
PWM phần mềm từ vòng lặp chínhKhông giới hạn1 tới 5 phần trămKhông
PWM phần mềm từ ngắtKhông giới hạn0.7 phần trămCó
Chip điều khiển đèn ngoài, ví dụ WS2812Hàng trămGần 0 nếu dùng DMACó

#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ộngHọc thêm được gìBài liên quan
Nhấn giữ để chỉnh độ sángSự kiện nhấn giữ, thay đổi liên tục theo thời gianBài 47.5
Lưu chế độ vào Flash để giữ sau khi mất điệnGhi Flash, đọc lại lúc khởi độngBài 50.4
Điều khiển qua UART thay vì nútBộ đệm vòng, tách lệnhBài 50.2
Chuyển PWM phần mềm sang phần cứngTimer, kênh so khớp, DMABài 47.7
Đèn phản ứng theo ánh sáng môi trườngADC, lọc nhiễu, số học dấu chấm cố địnhBài 50.3

Tự làm thử

  1. 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.
  2. Cài systick_khoi_tao và đo số vòng lặp chính mỗi giây, rồi ghi lại chu kỳ lâu nhất.
  3. 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_assert bắt được khi bạn thêm chế độ mà quên thêm dòng vào bảng.
  4. 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.
  5. 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_t là đủ 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_assert chặ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.