Linux Terminal Mode | canonical / nocanonical / cbreak / raw

注:本文为 “Linux 终端模式” 相关文章合辑

略作重排,如有内容异常,请看原文。


终端输入输出的三种模式

guidao

1 前言

在进行项目开发时,需要实时读取终端输入(无需按下 Enter 键即可读取)。然而,Go 语言并未提供便捷的接口。为此,查阅了相关资料,对终端的 I/O 进行了深入研究,并将关键内容整理如下。

2 终端 I/O 的三种模式

2.1 Canonical 模式

该模式也称为 cooked 模式 。在这种模式下,终端每次返回一行数据,所有特殊字符均会被解释(例如:^C)。

2.2 Nocanonical 模式

此模式亦称 raw 模式 。在这种模式下,终端每次返回一个字符,而非先收集一行数据再返回。特殊字符不会被特殊处理,需自行处理。例如,终端编辑器 vi 即处于这种模式下,可完全控制输入输出字符。

2.3 Cbreak 模式

该模式与 raw 模式相似,但会处理特殊字符(某些场景下需使用此模式)。

3 终端控制结构

终端设备的所有可控制属性均通过以下结构进行管理。Go 语言中,该结构定义于 syscall 包中。

struct termios {
    tcflag_t    c_iflag;    /* 输入标志 */
    tcflag_t    c_oflag;    /* 输出标志 */
    tcflag_t    c_cflag;    /* 控制标志 */
    tcflag_t    c_lflag;    /* 本地标志 */
    cc_t        c_cc[NCCS]; /* 控制字符 */
};

其中,c_iflag 控制输入属性(例如:将 CR 映射为 NL),c_oflag 控制输出属性(例如:将 tab 扩展为空格),c_cflag 控制行属性,c_lflag 设置用户与设备接口属性(例如:本地回显、允许信号产生)。该结构通过两个函数进行获取与设置,函数声明如下:

#include <termios.h>
int tcgetattr(int filedes, struct termios *termptr);
int tcsetattr(int filedes, int opt, const struct termios *termptr);
% 返回值:若成功则返回 0,若失败则返回 -1

filedes 是文件描述符,通常通过打开设备文件 /dev/tty 获得。tcsetattr 函数中的 opt 参数可取以下值:

  • TCSANOW :使设置立即生效。
  • TCSADRAIN :在输出缓存区输出到屏幕后生效。
  • TCSAFLUSH :在输出缓存区输出到屏幕后生效,并且丢弃所有输入缓存中未处理的数据。

4 纯 Go 实现一个 getchar(可立即获取用户输入而无需按下 Enter 键)

package main

import (
    "fmt"
    "os"
    "syscall"
    "unsafe"
)

% 通过系统调用设置属性
func ioctl(fd, request, argp uintptr) error {
    if _, _, e := syscall.Syscall6(syscall.SYS_IOCTL, fd, request, argp, 0, 0, 0); e != 0 {
        return e
    }
    return nil
}

% 获取设备属性
func Tcgetattr(fd uintptr, argp *syscall.Termios) error {
    return ioctl(fd, syscall.TIOCGETA, uintptr(unsafe.Pointer(argp)))
}

% 设置终端
func Tcsetattr(fd, opt uintptr, argp *syscall.Termios) error {
    return ioctl(fd, opt, uintptr(unsafe.Pointer(argp)))
}

func main() {
    var term syscall.Termios
    var origin syscall.Termios
    fd, err := os.Open("/dev/tty")
    must(err)
    err = Tcgetattr(fd.Fd(), &term)
    origin = term
    must(err)
    % 设置为nobuffer
    term.Lflag &^= syscall.ICANON
    term.Cc[syscall.VMIN] = 1
    term.Cc[syscall.VTIME] = 0
    Tcsetattr(fd.Fd(), syscall.TIOCSETA, &term)
    c, err := ReadChar(fd.Fd())
    must(err)
    fmt.Println(" read:", string(c))
    % 恢复原来的设置
    Tcsetattr(fd.Fd(), syscall.TIOCSETA, &origin)
}

func ReadChar(fd uintptr) ([]byte, error) {
    b := make([]byte, 4)
    n, e := syscall.Read(int(fd), b)
    if e != nil {
        return nil, e
    }
    return b[:n], nil
}
func must(err error) {
    if err != nil {
        panic(err)
    }
}

5 参考资料


终端的 Raw Mode

Erzbir

2025-02-10

Raw Mode

在启动 Terminal 时,默认是以 canonical mode 或者叫 cooked mode 启动的。这种模式下,用户按键产生的字符会由终端驱动存入内部缓冲区,期间可以自动处理 BackspaceCtrl-C 等特殊字符,需要等待用户按下 Enter 后才将字符从缓冲区中送到程序的标准输入(STDIN_FILENO 为 0)。

例如下面这个程序:

#include <unistd.h>

int main() {
    char c;
    while (read(STDIN_FILENO, &c, 1) == 1) {
        write(STDOUT_FILENO, &c, 1);
    }
    return 0;
}

终端默认是 canonical mode ,所以会在按下 Enter 后,从标准输入中每次读取一个字节到 c ,直到 0 个字节可读。

这也是常用的一种模式,输入命令及参数回车,然后 shell 就会解释我们的命令。但是你可能见过一些命令行程序比如 top ,按下 q 后并没有 Enter 就直接退出了,这就需要 raw mode 让我们自己来处理这些输入。

下面这个例子仍然是 canonical mode 的,也是通过 q 来退出,但是需要 Enter

#include <unistd.h>

int main() {
    char c;
    while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q') {
        write(STDOUT_FILENO, &c, 1);
    }
    return 0;
}

但是这样的问题就在于和预期稍微有些差别,我们不仅要 Enter ,而且那之后的字符可能是 wqeqweq 这样,而这个程序是读到一个 q char 就会退出。

而在 raw mode 下,没有回显,不会处理特殊字符,也没有缓冲区。

关闭回显

要切换到 raw mode 首先是关闭回显,需要用到 termios.h 提供的 tcgetattr() 函数以及 tcsetattr() 函数。

这两个函数的定义是这样的:

% The tcgetattr() function copies the parameters associated with the terminal referenced by fildes in the termios structure referenced by termios_p.
% This function is allowed from a background process; however, the terminal attributes may be subsequently changed by a foreground process.

int tcgetattr(int, struct termios *);

% The tcsetattr() function sets the parameters associated with the terminal from the termios structure referenced by termios_p.
% The optional_actions field is created by or'ing the following values, as specified in the include file ⟨termios.h⟩.

int tcsetattr(int, int, const struct termios *);

通过下面这三个语句就可以获取到当前的状态:

struct termios raw;
tcgetattr(STDIN_FILENO, &raw);
printf("%lu", raw.c_lflag);

我们需要的就是这个 c_lflag ,将这个 flag 进行位运算后再调用 tcsetattr() 设置即可:

void enable_raw_mode() {
    struct termios raw;
    tcgetattr(STDIN_FILENO, &raw);
    raw.c_lflag &= ~(ECHO);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

这样就关闭了回显,运行之后输入字符就不会再有回显。如果你发现还是有回显,确保在终端运行,而不是用 IDE 的 run 来运行或是其他的一些 wrap。

切换到 Raw Mode

我们已经关闭了回显,现在还需要禁用缓冲区和禁用特殊字符处理等等操作,下面一步一步来。

禁用 SIGINT 和 SIGSTP

ISIG 控制是否发送 SIGINTSIGSTP

void enable_raw_mode() {
    struct termios raw;
    tcgetattr(STDIN_FILENO, &raw);
    raw.c_lflag &= ~(ECHO | ICANON | ISIG);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

现在,不需要回车且 Ctrl-CCtrl-Z 的输入已经不起作用。

禁用输出流控^Software flow control^

通过 flag: c_iflagIXON 控制。

void enable_raw_mode() {
    struct termios raw;
    tcgetattr(STDIN_FILENO, &raw);
    raw.c_lflag &= ~(ECHO | ICANON | ISIG);
    raw.c_iflag &= ~(IXON);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

禁用 Ctrl-V

通过 c_lflagIEXTEN 控制。

void enable_raw_mode() {
    struct termios raw;
    tcgetattr(STDIN_FILENO, &raw);
    raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
    raw.c_iflag &= ~(IXON);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

关闭换行映射

c_lflagICRNL

\r 的 char 值是 13 ,\n 的 char 值是 10 。

为了消除不同操作系统之间换行符的差异,默认开启映射,可能会将 \r 自动转换成 \n ,也就是说 Ctrl-JCtrl-M 会是一样的值,而 Ctrl-M 原本是 13 。

void enable_raw_mode() {
    struct termios raw;
    tcgetattr(STDIN_FILENO, &raw);
    raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
    raw.c_iflag &= ~(IXON | ICRNL);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

关闭输出处理

c_oflagOPOST

开启时终端会将换行符 \n 转换成回车符后跟一个换行符:\r\n

void enable_raw_mode() {
    struct termios raw;
    tcgetattr(STDIN_FILENO, &raw);
    raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
    raw.c_iflag &= ~(IXON | ICRNL);
    raw.c_oflag &= ~(OPOST);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

实际上还需要更多的操作,比如通过结构体中的 c_cc 来设置 read() 返回所需要的最小字节数以及读超时时间等:

void enable_raw_mode() {
    struct termios raw;
    tcgetattr(STDIN_FILENO, &raw);

    % 修改 raw.c_lflag 来禁用终端的一些本地模式
    % 禁用 ECHO: 关闭回显
    % 禁用 ICANON: 关闭规范模式 (行缓冲), 使得输入按字符处理
    % 禁用 IEXTEN: 禁用扩展输入处理
    % 禁用 ISIG: 禁用信号生成 (例如 Ctrl+C 不再生成 SIGINT 信号)
    raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);

    % 修改 raw.c_iflag 来禁用某些输入处理
    % 禁用 BRKINT, ICRNL, INPCK, ISTRIP, IXON 等标志
    raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP);

    % 修改 raw.c_oflag 来禁用输出处理 (比如自动转换换行符)
    raw.c_oflag &= ~(OPOST);

    % 修改 raw.c_cflag" 设置字符大小为 8(通常为 CS8)
    raw.c_cflag |= (CS8);

    % 设置控制字符
    % VMIN = 0: read() 至少读取 0 个字节
    % VTIME = 1000: read() 超时时间为 1000ms
    raw.c_cc[VMIN] = 0;
    raw.c_cc[VTIME] = 1000;

    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

int main() {
    char c;
    enable_raw_mode();
    while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q') {
        if (c == '\r' | c == '\n') {
            write(STDOUT_FILENO, "\r\n", 2);
        }
        write(STDOUT_FILENO, &c, 1);
    }
    return 0;
}

这里就设置了 1000 秒后如果没有读取到 read() 就会直接返回,如果 c_cc[VMIN] 为 1 则是会阻塞到读取到一个字节为止。

  • BRKINT :当检测到 break 条件时触发中断行为或产生错误,帮助捕捉和处理输入中断。
  • INPCK :启用输入奇偶校验,用于检测数据传输中的错误。
  • ISTRIP :对输入数据进行剥离,将每个字节的最高位清零,确保只保留 7 位数据。

直接切换到 Raw Mode

实际上 termios.h 中有提供一个直接切换的 API :cfmakeraw()

这个函数只需要传入一个 struct termios ,会修改结构体中的值就像我们上面做的那样。

void enable_raw_mode() {
    struct termios raw;
    cfmakeraw(&raw);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

使用其他语言

使用其他语言就要用到 FFI 了,比如下面使用 Rust ,好在 Rust 提供了一个 libc 库可以很容易做到:

fn enable_raw_mode() {
    unsafe {
        let mut termios = {
            std::mem::zeroed()
        };

        libc::tcgetattr(libc::STDIN_FILENO, &mut termios);
        libc::cfmakeraw(&mut termios);
        libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &termios);
    }
}

这只是一个最简单的例子。

下面是纯 FFI 的例子:

#[repr(C)]
#[derive(Copy, Clone)]
struct Termios {
    c_iflag: u32,
    c_oflag: u32,
    c_cflag: u32,
    c_lflag: u32,
    cc_c: [u8; 20],
    c_ispeed: u32,
    c_ospeed: u32,
}

extern "C" {
    fn tcgetattr(fd: i32, termios: &mut Termios) -> i32;
    fn tcsetattr(fd: i32, command: i32, termios: &Termios) -> i32;
    fn cfmakeraw(termios: &mut Termios);
}

const STDIN_FILENO: i32 = 0;
const TCSAFLUSH: i32 = 2;

fn enable_raw_mode() {
    unsafe {
        let mut termios = std::mem::zeroed();
        cfmakeraw(&mut termios);
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &mut termios);
    }
}

Linux 串口设备的 Canonical and noncanonical mode

posted @ 2021-10-17 12:31 cnwanglu

Canonical and noncanonical mode

Canonical 与 noncanonical 模式可能翻译为规范/非规范模式。

通过 termios 结构体的 c_lflag 成员来设置 Canonical 或 noncanonical 模式。

这两种模式是与串口输入相关的配置。

Canonical 模式中

  • 串口读取的单位是行,行定义为包含行结束符:NLEOLEOL2;或在行起始处的 EOFread() 函数收到结束符后即返回,其中除了 EOF 之外(如回车符等),这些结束符与普通字符一样会被 read() 函数读到缓冲区中。
  • 如果 read 函数在没有读到结束符之前意外终止,缓存的数据将被保留。下次调用 read 函数时将包含这些数据。

noncanonical 模式中

  • 串口读取的单位是字节,不需要等待任何的结束符,而是由 MINc_cc[VMIN])和 TIMEc_cc[VTIME])的设置来决定 read 函数何时返回。

VMIN 和 VTIME

给出定义:

  • VMIN:在 noncanonical 模式中规定 read() 函数返回的最小字节数。
  • VTIME:在 noncanonical 模式中规定 read() 函数返回的超时时间(以 100ms 为单位)。

比如说,存在以下几种情况:

MIN == 0, TIME == 0 (polling read)

有数据就返回,返回值是 min ⁡ { \min \{ min{当前可用数据长度,请求数据长度 } \} },若没有数据则返回 0,不等待。

请求数据长度:count in ssize_t read (int fd, void *buf, size_t count);

MIN > 0, TIME == 0 (blocking read)

直到接收到 MIN 个字节时,read 函数才返回。

但是若请求数据长度小于 MINread 函数会在读到请求数据长度时提前返回。

MIN == 0, TIME > 0 (read with timeout)

调用 read() 函数时开始计时,在 TIME 时间内,若收到数据就返回。

MIN > 0, TIME > 0 (read with interbyte timeout)

在收到字节的数据后开始计时。read() 函数返回的原因包括:

  1. TIME 时间内,MIN 长度的数据已到达。
  2. TIME 计时时间到。
  3. TIME 时间内,请求数据长度的数据已到达。(POSIX 未规定此情况,所以有可能其他系统未实现!)

所以在此类情况下,造成 read() 函数返回的返回值必然大于 0,否则计时器不会开启,read() 函数会阻塞在那里。

补充:Linux 中标准输入 / 输出的行缓存

在 Linux 中有时调用 printf() 函数打印却不显示,可能是因为没有加换行符(\n)导致的,这是因为 Linux 中关于标准输入 / 输出默认是启用行缓存的。其实这与我们上面提到的 Canonical 模式是同样的原理。

解决的方案有三个:

  1. 手动添加换行符(\n);这种方式具有局限性,因为有的时候换行符会对原始数据造成干扰。
  2. 刷新标准输出缓冲区;
    执行 fflush (stdout) 将目前缓冲区中的数据发送给输出设备,来刷新缓冲区。
  3. 关闭行缓存;
setvbuf (stdout,NULL,_IONBF,0);

通过这种方式暴力关闭输出缓冲区,这样接收到的数据就会直接发送给输出设备了。


via:

/* * fn CLI_RET cli_input_parse(char *buff, size_t blen, char *prompt) * brief get user input command and parse it * details * * param[in] * param[out] buff user input commands buffer prompt current prompt string to display * * return excute status * retval CLI_RET_OK, CLI_RET_ERROR * * note this is the main function for input-parser mod */ CLI_RET cli_input_parse(char *buff, size_t blen, char *prompt) { unsigned char c = 0; int arrow = 0; int break_out = 0; int y = 0; char *pCmd = NULL; /* disable canonical mode, no echo */ ioctl(0, FIOSETOPTIONS, OPT_RAW); ioctl(0, FIOSETOPTIONS, OPT_CRMOD); handlers_sets |= SET_RESET_TERM; /* init terminal */ buff[0] = 0; l_cursor_y = 0; l_cmd_len = 0; l_pCmd = buff; l_cmdSize = blen; cli_input_init(); /* put prompt */ cli_put_prompt(prompt); /* loop for read input char */ while (1) { fflush(stdout); if (cli_get_input(0, &c, 1) < 1) { break_out = -1; goto input_exit; break; } /* �����յ����Ƕ����Ʋ��� ��ô \r \n �������⡣��Ҫ������telnet����� by Li Chenglong*/ /*����"*/ /* parse input char */ switch(c) { case '\r': case '\n': cli_input_end(); putchar('\n'); break_out = 1; break; /* Control-a -- Beginning of line */ case 1: cli_cursor_backward(l_cursor); break; /* Control-b -- Move back one character */ case 2: cli_cursor_backward(1); break; /* Control-c -- stop get input */ case 3: putchar('\n'); memset(buff, 0, blen); cli_put_prompt(prompt); l_up_index = ((l_history_num - 1) % CLI_MAX_HISTORY_NUM); l_down_index = -1; break; /* Control-d -- Delete one character, or exit */ case 4: if (l_cmd_len == 0) { cli_input_end(); putchar('\n'); /* by Li Chenglong*/ cli_processLogOut(); } else { cli_input_delete(); } break; /* Control-e -- to the end of line */ case 5: cli_input_end(); break; /* Control-f -- Move forward one character */ case 6: cli_cursor_forward(); break; /* backspace and Control-h */ case '\b': case DEL: { cli_input_backspace(); break; } /* Control-k -- clear to the end of line */ case 11: *(l_pCmd + l_cursor) = 0; l_cmd_len = l_cursor; CLI_PRINT("\033[J"); break; /* Control-l -- clear screen */ case 12: CLI_PRINT("\033[1H\033[2J"); cli_redraw(0, l_cmd_len - l_cursor); break; /* input tab */ case '\t': /* have not login */ if (g_cli_mode == CLI_MODE_NONE) { break; } #ifdef __CMD_TABLE__ buff[l_cmd_len] = '\0'; cli_parseTab(buff, blen); l_cmd_len = strlen(buff); cli_redraw(l_cursor_y, 0); #endif break; /* Control-n -- Get next command in history */ case 14: arrow = CLI_DOWN_ARROW; goto history_redraw; break; /* Control-p -- Get previous command from history */ case 16: arrow = CLI_UP_ARROW; goto history_redraw; break; /* Control-U -- Clear line before cursor */ case 21: if (l_cursor) { CUTIL_STR_STRNCPY(l_pCmd, l_pCmd + l_cursor, l_cmdSize); cli_redraw(l_cursor_y, l_cmd_len -= l_cursor); } break; /*������ͨ�ù涨�Ŀ�ݲ�������man ascii �п����ҵ���ϸ������.*/ /*������vt100 �����ն�֧�ֵ�ESC ��������*/ /* VT100 --- control mode */ case ESC: { if (cli_get_input(fileno(stdin), &c, 1) < 1) { break_out = -1; goto input_exit; } if (c == '[' || c == 'O') { if (cli_get_input(fileno(stdin), &c, 1) < 1) { break_out = -1; goto input_exit; } } switch (c) { case '\n': case '\r': /* Enter */ l_cmd_len = 0; l_pCmd[l_cmd_len++] = ESC; break_out = 1; break; case 'A': /*up arrow*/ arrow = CLI_UP_ARROW; goto history_redraw; break; case 'B': arrow = CLI_DOWN_ARROW; history_redraw: pCmd = cli_load_history(arrow); if (!pCmd) { bzero(buff, blen); l_cursor = l_cursor_x = l_cursor_y = l_cmd_len = 0; putchar('\r'); cli_put_prompt(prompt); CLI_PRINT("\033[J"); } else { CUTIL_STR_STRNCPY(buff, pCmd, blen); y = l_cursor_y; l_cursor = l_cursor_x = l_cursor_y = 0; l_cmd_len = strlen(pCmd); cli_redraw(y, 0); } break; case 'C': cli_cursor_forward(); break; case 'D': cli_cursor_backward(1); break; /* Delete */ case '3': /* remove symbol ~ */ if (cli_get_input(fileno(stdin), &c, 1) < 1) { break_out = -1; goto input_exit; } cli_input_delete(); break; /* Home (Ctrl-A) */ case '1': case 'H': /*remove symbol ~*/ if (cli_get_input(fileno(stdin), &c, 1) < 1) { break_out = -1; goto input_exit; } cli_cursor_backward(l_cursor); break; /* End (Ctrl-E) */ case '4': case 'F': /*remove symbol ~*/ if (cli_get_input(fileno(stdin), &c, 1) < 1) { break_out = -1; goto input_exit; } cli_input_end(); break; case '2': case '5': case '6': /*remove symbol ~*/ if (cli_get_input(fileno(stdin), &c, 1) < 1) { break_out = -1; goto input_exit; } break; default: if (!(c >= '1' && c <= '9')) { c = 0; } CLI_BEEP(); break; } break; } default: /* normal characters */ if (c == 22) { if (cli_get_input(0, &c, 1) < 1) { break_out = -1; goto input_exit; } if (c == 0) { CLI_BEEP(); break; } } else if (!Isprint(c)) { break; } if (l_cmd_len > CLI_MAX_CMD_LEN - 2) { break; } l_cmd_len++; if (l_cursor == l_cmd_len - 1) /* append at the end of the line */ { *(l_pCmd + l_cursor) = c; *(l_pCmd + l_cursor + 1) = 0; cli_setout_char(0); } else /* insert otherwise */ { int sc = l_cursor; memmove(l_pCmd + sc + 1, l_pCmd + sc, l_cmd_len - sc); *(l_pCmd + sc) = c; sc++; cli_input_end(); cli_cursor_backward(l_cursor - sc); } break; } if (break_out == 1) { break; } } input_exit: /* reset terminal */ setTermSettings(0, (void *) &initialSettings); handlers_sets &= ~SET_RESET_TERM; if (break_out > 0) { buff[l_cmd_len] = '\0'; cli_save_history(buff); } cli_reset_term(); return CLI_RET_OK; }解释一下这个函数的作用,以及在这里是什么作用 /*just get input to the line end.*/ cli_input_parse(username, sizeof(username), "username:");
11-26
提供了一个基于51单片机的RFID门禁系统的完整资源文件,包括PCB图、原理图、论文以及源程序。该系统设计由单片机、RFID-RC522频射卡模块、LCD显示、灯控电路、蜂鸣器报警电路、存储模块和按键组成。系统支持通过密码和刷卡两种方式进行门禁控制,灯亮表示开门成功,蜂鸣器响表示开门失败。 资源内容 PCB图:包含系统的PCB设计图,方便用户进行硬件电路的制作和调试。 原理图:详细展示了系统的电路连接和模块布局,帮助用户理解系统的工作原理。 论文:提供了系统的详细设计思路、实现方法以及测试结果,适合学习和研究使用。 源程序:包含系统的全部源代码,用户可以根据需要进行修改和优化。 系统功能 刷卡开门:用户可以通过刷RFID卡进行门禁控制,系统会自动识别卡片并判断是否允许开门。 密码开门:用户可以通过输入预设密码进行门禁控制,系统会验证密码的正确性。 状态显示:系统通过LCD显示屏显示当前状态,如刷卡成功、密码错误等。 灯光提示:灯亮表示开门成功,灯灭表示开门失败或未操作。 蜂鸣器报警:当刷卡或密码输入错误时,蜂鸣器会发出报警声,提示用户操作失败。 适用人群 电子工程、自动化等相关专业的学生和研究人员。 对单片机和RFID技术感兴趣的爱好者。 需要开发类似门禁系统的工程师和开发者。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值