色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

串口驅動分析之serial driver

嵌入式與Linux那些事 ? 來源:嵌入式與Linux那些事 ? 2024-09-04 14:23 ? 次閱讀

簡介

前兩節我們介紹串口驅動的框架和tty core部分。這節我們介紹和硬件緊密相關的串口驅動部分。

UART驅動部分依賴于硬件平臺,而TTY驅動和具體的平臺無關。雖然UART部分依賴于平臺,但是不管是哪個硬件平臺,驅動的思路都是一致的,下面分模塊來分別介紹。

關鍵數據結構

struct uart_driver

struct uart_driver結構體本身并不包含底層UART硬件的操作方法,其是所有串口設備驅動的抽象和封裝。起到了連接硬件設備驅動和TTY驅動的作用。注冊了struct uart_driver后還不能使用UART設備,還需要關聯具體的UART設備。

uart_driver 結構體表示 UART 驅動, 它定義在include/linux/serial_core.h文件中,內容如下:

structuart_driver{
structmodule*owner;
constchar*driver_name;
constchar*dev_name;
intmajor;
intminor;
intnr;
structconsole*cons;

/*
*theseareprivate;thelowleveldrivershouldnot
*touchthese;theyshouldbeinitialisedtoNULL
*/
structuart_state*state;
structtty_driver*tty_driver;
};

owner:指向該驅動程序的擁有者模塊的指針,即加載該驅動程序的內核模塊。

driver_name:字符串,表示驅動程序的名稱。

dev_name:字符串,表示設備名稱,即驅動程序控制的設備文件的名稱,比如ttyS。

major:表示設備文件的主設備號。

minor:表示設備文件的次設備號。

nr:整數,表示該驅動程序控制的設備數量。

cons:指向 struct console 類型的指針,表示該串口設備所綁定的控制臺。

此外,結構體中還包含了兩個私有的指針字段:

state:指向 struct uart_state 類型的指針,表示該驅動程序內部的狀態信息

tty_driver:指向 struct tty_driver 類型的指針,表示該驅動程序所對應的 tty 驅動程序。

struct uart_port

一個串口芯片上往往有多個串行端口(serial ports,對應于一個物理上的串口),這些串行端口具備相同的操作機制。Linux內核將這些串行端口用struct uart_port結構體描述。struct uart_port用于描述一個UART端口的中斷、I/O內存地址、FIFO大小、端口類型等信息。

在 Linux 內核中,每個串口設備都會對應一個 struct uart_port 數據結構,并且這個數據結構會作為串口設備的一個屬性被保存在相應的設備節點中。

當應用程序通過打開設備節點來訪問串口設備時,內核會通過設備節點獲取對應的 struct uart_port 數據結構,然后通過這個數據結構來進行串口的讀寫等操作。

structuart_port{
spinlock_tlock;/*portlock*/
unsignedlongiobase;/*in/out[bwl]*/
unsignedchar__iomem*membase;/*read/write[bwl]*/
unsignedint(*serial_in)(structuart_port*,int);
void(*serial_out)(structuart_port*,int,int);
void(*set_termios)(structuart_port*,
structktermios*new,
structktermios*old);
void(*set_mctrl)(structuart_port*,unsignedint);
int(*startup)(structuart_port*port);
void(*shutdown)(structuart_port*port);
void(*throttle)(structuart_port*port);
void(*unthrottle)(structuart_port*port);
int(*handle_irq)(structuart_port*);
void(*pm)(structuart_port*,unsignedintstate,
unsignedintold);
void(*handle_break)(structuart_port*);
int(*rs485_config)(structuart_port*,
structserial_rs485*rs485);
unsignedintirq;/*irqnumber*/
unsignedlongirqflags;/*irqflags*/
unsignedintuartclk;/*baseuartclock*/
unsignedintfifosize;/*txfifosize*/
unsignedcharx_char;/*xon/xoffchar*/
unsignedcharregshift;/*regoffsetshift*/
unsignedchariotype;/*ioaccessstyle*/
unsignedcharunused1;

unsignedintread_status_mask;/*driverspecific*/
unsignedintignore_status_mask;/*driverspecific*/
structuart_state*state;/*pointertoparentstate*/
structuart_icounticount;/*statistics*/
structconsole*cons;/*structconsole,ifany*/
/*flagsmustbeupdatedwhileholdingportmutex*/
upf_tflags;
/*
*Mustholdtermios_rwsem,portmutexandportlocktochange;
*canholdanyonelocktoread.
*/
upstat_tstatus;

inthw_stopped;/*sw-assistedCTSflowstate*/
unsignedintmctrl;/*currentmodemctrlsettings*/
unsignedinttimeout;/*character-basedtimeout*/
unsignedinttype;/*porttype*/
conststructuart_ops*ops;
unsignedintcustom_divisor;
unsignedintline;/*portindex*/
unsignedintminor;
resource_size_tmapbase;/*forioremap*/
resource_size_tmapsize;
structdevice*dev;/*parentdevice*/
unsignedcharhub6;/*thisshouldbeinthe8250driver*/
unsignedcharsuspended;
unsignedcharirq_wake;
unsignedcharunused[2];
structattribute_group*attr_group;/*portspecificattributes*/
conststructattribute_group**tty_groups;/*allattributes(serialcoreuseonly)*/
structserial_rs485rs485;
void*private_data;/*genericplatformdatapointer*/
};

unsigned long iobase: 指定了該串口設備在I/O空間中的基地址。

unsigned char __iomem *membase: 指向該串口設備在內存中映射的地址。

unsigned int (*serial_in)(struct uart_port *, int): 函數指針,用于從串口設備中讀取數據。

void (*serial_out)(struct uart_port *, int, int): 函數指針,用于向串口設備中寫入數據。

void (*set_termios)(struct uart_port *, struct ktermios *new, struct ktermios *old): 函數指針,用于設置串口設備的終端參數。

void (*set_mctrl)(struct uart_port *, unsigned int): 函數指針,用于設置串口設備的 modem 控制信號

int (*startup)(struct uart_port *port): 函數指針,用于初始化串口設備并啟動傳輸。

void (*shutdown)(struct uart_port *port): 函數指針,用于關閉串口設備。

void (*throttle)(struct uart_port *port): 函數指針,用于將串口設備的傳輸流控制為停止狀態。

void (*unthrottle)(struct uart_port *port): 函數指針,用于取消串口設備的傳輸流控制停止狀態。

int (*handle_irq)(struct uart_port *): 函數指針,用于處理串口設備的中斷。

void (*pm)(struct uart_port *, unsigned int state, unsigned int old): 函數指針,用于處理串口設備的電源管理

void (*handle_break)(struct uart_port *): 函數指針,用于處理串口設備的中斷信號中斷符。

int (*rs485_config)(struct uart_port *, struct serial_rs485 *rs485): 函數指針,用于配置 RS485 串行通信參數

unsigned int irq: 該串口設備所使用的中斷號。

unsigned long irqflags: 該串口設備的中斷標志。

unsigned int uartclk: 該串口設備的時鐘頻率。

unsigned int fifosize: 該串口設備的 FIFO 大小。

unsigned char x_char: XON/XOFF 字符。

unsigned char regshift: 寄存器偏移量。

unsigned char iotype: I/O 訪問類型。

unsigned char unused1: 未使用的成員變量。

unsigned int read_status_mask: 用于指定讀取狀態的屏蔽位。

unsigned int ignore_status_mask: 用于指定忽略狀態的屏蔽位。

struct uart_state *state: 指向該串口設備所在狀態結構體的指針。

struct uart_icount icount: 用于存儲串口設備的統計信息。

struct console *cons: 指向該串口設備所屬控制臺設備的指針。

unsigned int mctrl:當前調制解調器控制(Modem Control)的設置。這個值包含了當前控制信號(如DTR、RTS、DSR、CTS等)的狀態。通常由硬件控制。

unsigned int timeout:基于字符的超時時間。當字符被傳輸到UART端口時,如果在規定的時間內沒有收到下一個字符,則會超時并發送通知。通常由驅動程序設置。

unsigned int type:端口類型。這個值通常用于標識UART硬件的特殊性質(如芯片類型、波特率范圍等)。

const struct uart_ops *ops:一個指向struct uart_ops結構體的指針。這個結構體包含了與UART驅動程序相關的函數指針,如UART讀、寫、啟動、停止等等。

unsigned int custom_divisor:自定義除數,用于實現非標準波特率。這個值通常由驅動程序設置。

unsigned int line:端口索引,用于標識該UART端口的編號。

unsigned int minor:端口的次設備號,用于標識該UART端口在系統中的位置。

resource_size_t mapbase、resource_size_t mapsize:映射區域的起始地址和大小。這些值通常由驅動程序設置,用于將UART端口的物理地址映射到虛擬地址。

struct device *dev:指向父設備的指針。通常是該UART設備所連接的總線控制器設備。

unsigned char hub6:用于指示Hub6電路板的狀態。這個變量應該是在8250驅動程序中定義的。

unsigned char suspended:用于指示該端口是否被掛起。

unsigned char irq_wake:用于指示該端口是否支持喚醒中斷。

unsigned char unused[2]:未使用的字節。

struct attribute_group *attr_group:指向屬性組的指針。屬性組包含了UART設備的屬性和操作,如設備狀態、波特率設置等等。

const struct attribute_group **tty_groups:指向指針數組的指針,該數組包含了所有屬性組的指針,供串行核心使用。

struct serial_rs485 rs485:RS485配置結構體,用于RS485通信。

void *private_data:指向私有數據的指針。這個指針通常由驅動程序使用,用于保存驅動程序特定的數據。

struct uart_ops

Linux 系統收發數據最終調用的都是 ops 中的函數。 ops 是 uart_ops類型的結構體指針變量。uart硬件操作函數集合,底層硬件驅動必須實現這個結構體。

uart_ops結構體 用于定義一個串口驅動程序的接口,讓上層調用這些接口實現串口的讀寫等操作。它包含了很多函數指針,每個函數指針對應了一個特定的串口操作。

在Linux內核中,串口的驅動程序是分為兩層實現的:串口芯片驅動程序和 serial core 層。其中,serial core 層提供了大量的函數接口,供上層的串口芯片驅動程序使用,這些函數接口的定義就包含在了 struct uart_ops 結構體中。

當編寫串口芯片驅動程序時,需要實現 struct uart_ops 結構體中定義的各個函數接口,以便 serial core 層調用。

例如,在芯片驅動程序中實現的 uart_start() 函數就對應了 struct uart_ops 結構體中的 startup 函數指針。

因此,struct uart_ops 結構體是串口驅動程序實現的關鍵,其定義了驅動程序需要實現的所有函數接口,并與 serial core 層進行了對接。

structuart_ops{
unsignedint(*tx_empty)(structuart_port*);
void(*set_mctrl)(structuart_port*,unsignedintmctrl);
unsignedint(*get_mctrl)(structuart_port*);
void(*stop_tx)(structuart_port*);
void(*start_tx)(structuart_port*);
void(*throttle)(structuart_port*);
void(*unthrottle)(structuart_port*);
void(*send_xchar)(structuart_port*,charch);
void(*stop_rx)(structuart_port*);
void(*enable_ms)(structuart_port*);
void(*break_ctl)(structuart_port*,intctl);
int(*startup)(structuart_port*);
void(*shutdown)(structuart_port*);
void(*flush_buffer)(structuart_port*);
void(*set_termios)(structuart_port*,structktermios*new,
structktermios*old);
void(*set_ldisc)(structuart_port*,structktermios*);
void(*pm)(structuart_port*,unsignedintstate,
unsignedintoldstate);
void(*wake_peer)(structuart_port*);

/*
*Returnastringdescribingthetypeoftheport
*/
constchar*(*type)(structuart_port*);

/*
*ReleaseIOandmemoryresourcesusedbytheport.
*Thisincludesiounmapifnecessary.
*/
void(*release_port)(structuart_port*);

/*
*RequestIOandmemoryresourcesusedbytheport.
*Thisincludesiomappingtheportifnecessary.
*/
int(*request_port)(structuart_port*);
void(*config_port)(structuart_port*,int);
int(*verify_port)(structuart_port*,structserial_struct*);
int(*ioctl)(structuart_port*,unsignedint,unsignedlong);
#ifdefCONFIG_CONSOLE_POLL
int(*poll_init)(structuart_port*);
void(*poll_put_char)(structuart_port*,unsignedchar);
int(*poll_get_char)(structuart_port*);
#endif
};

tx_empty():檢查串口的發送緩沖區是否為空,用于判斷是否可以發送數據。

set_mctrl():設置串口的 modem 控制信號,如 RTS、DTR 等。

get_mctrl():獲取串口的 modem 控制信號。

stop_tx():停止當前正在發送的數據。

start_tx():開始發送數據。

throttle():限制發送速率,減少發送的數據量。

unthrottle():取消限制發送速率。

send_xchar():發送一個 XON 或 XOFF 字符,用于流控。

stop_rx():停止接收數據。

enable_ms():啟用串口的 modem 狀態檢測功能。

break_ctl():發送一個 break 信號。

startup():初始化串口硬件。

shutdown():關閉串口硬件。

flush_buffer():清空串口的緩沖區。

set_termios():設置串口的終端參數。

set_ldisc():設置串口的行規則。

pm():實現串口的 power management。

wake_peer():用于喚醒其他休眠狀態的串口。

另外,還包含了一些函數指針用于處理串口的 IO 資源:

type():返回描述串口類型的字符串。

release_port():釋放串口的 IO 和內存資源,包括解除 IO 映射等。

request_port():請求串口的 IO 和內存資源,包括 IO 映射等。

config_port():配置串口的參數。

verify_port():驗證串口的參數是否正確。

ioctl():實現串口設備的 ioctl 接口。

struct uart_state

uart_state 表示 UART 狀態,并與 struct uart_port 結構體配合使用來管理 UART 端口。

struct uart_port 結構體表示 UART 端口的硬件信息和操作,而 struct uart_state 結構體則表示與該端口相關的軟件狀態。

由于 UART 狀態可以包含多個,因此可以在同一時刻使用多個 UART 狀態來管理多個 UART 端口的操作。

structuart_state{
structtty_portport;

enumuart_pm_statepm_state;
structcirc_bufxmit;

structuart_port*uart_port;
};

struct tty_port port:表示 tty 端口的狀態信息,包括接受和發送緩沖區,控制信息和流控信息等等。

enum uart_pm_state pm_state:表示串口設備的電源管理狀態,可以是 UART_PM_STATE_ON、UART_PM_STATE_OFF 或 UART_PM_STATE_UNDEFINED。

struct circ_buf xmit:表示串口設備的發送緩沖區,用于存儲待發送的數據。

struct uart_port *uart_port:表示該串口設備對應的 struct uart_port 結構體。

當應用程序向串口設備寫入數據時,數據將被存儲到 xmit 緩沖區中,并且將觸發串口驅動程序的數據發送處理函數。這個函數會從 xmit 緩沖區中取出數據,并通過 uart_port 中的函數指針將數據發送到物理串口。在發送數據時,驅動程序還會根據串口的流控狀態進行數據流控制。

當收到數據時,數據將被存儲到 port 的接受緩沖區中,并且將觸發串口驅動程序的數據接收處理函數。處理函數將從接受緩沖區中取出數據并將其傳遞給應用程序。

數據結構抽象完畢后,serial core向下層的driver提供了方便的編程API,主要包括以下函數。

關鍵API

uart_register_driver

uart_register_driver將定義并填充好的uart driver注冊到kernel中,一般在驅動模塊的init接口中被調用。

intuart_register_driver(structuart_driver*drv)
{
structtty_driver*normal;
inti,retval;

BUG_ON(drv->state);

drv->state=kzalloc(sizeof(structuart_state)*drv->nr,GFP_KERNEL);
if(!drv->state)
gotoout;

normal=alloc_tty_driver(drv->nr);
if(!normal)
gotoout_kfree;

drv->tty_driver=normal;

normal->driver_name=drv->driver_name;
normal->name=drv->dev_name;
normal->major=drv->major;
normal->minor_start=drv->minor;
normal->type=TTY_DRIVER_TYPE_SERIAL;
normal->subtype=SERIAL_TYPE_NORMAL;
normal->init_termios=tty_std_termios;
normal->init_termios.c_cflag=B9600|CS8|CREAD|HUPCL|CLOCAL;
normal->init_termios.c_ispeed=normal->init_termios.c_ospeed=9600;
normal->flags=TTY_DRIVER_REAL_RAW|TTY_DRIVER_DYNAMIC_DEV;
normal->driver_state=drv;
tty_set_operations(normal,&uart_ops);

/*
*InitialisetheUARTstate(s).
*/
for(i=0;inr;i++){
structuart_state*state=drv->state+i;
structtty_port*port=&state->port;

tty_port_init(port);
port->ops=&uart_port_ops;
}

retval=tty_register_driver(normal);
if(retval>=0)
returnretval;

for(i=0;inr;i++)
tty_port_destroy(&drv->state[i].port);
put_tty_driver(normal);
out_kfree:
kfree(drv->state);
out:
return-ENOMEM;
}

uart_register_driver()注冊所做工作如下:

根據driver支持的最大設備數,申請n個 uart_state 空間,每一個 uart_state 都有一個 uart_port 。

接著它會分配一個 tty_driver 對象,并初始化它的各個屬性,如 driver_name,name,major,minor_start 等等。這些屬性是用于在 TTY 子系統中創建 tty 設備的,它們的值來自于 uart_driver 對象中指定的值。

接下來,它會在 tty_driver 中設置 tty 操作,其中 tty_ops 是一個結構體,定義了 UART 串行接口所需要的函數。這些函數在串口設備注冊后,當有數據進出串口時,TTY 子系統會調用這些函數。tty_set_operations() 函數用于在 tty_driver 中設置 tty 操作。

在初始化完 tty_driver 后,函數會遍歷所有的 UART 設備狀態對象,并初始化它們。這些狀態對象被存儲在 uart_driver 對象的 state 字段中。每個 UART 設備狀態對象包含一個 tty_port 對象,其中存儲了關于該串口設備的信息,例如流控、字長、奇偶校驗等等。在此處, tty_port 的操作被設置為 uart_port_ops,它包含了具體實現 UART 串行接口所需的函數。

最后會調用 tty_register_driver() 函數來向內核注冊 tty 驅動程序,并將驅動程序的 tty_driver 結構體與 uart_driver 結構體相關聯。

如果注冊失敗,該函數將釋放之前分配的內存。如果注冊成功,該函數將返回 0,否則將返回一個負的錯誤碼。

總結一句話:tty serial core底層驅動層和tty層之間的聯系需要從uart_register_driver()中連接,tty_driver是在uart_driver注冊過程中構建的。

uart_unregister_driver

uart_unregister_driver是一個Linux內核中的串口驅動反注冊函數,用于將之前注冊的驅動程序與系統中的串口設備取消關聯。

/**
*uart_unregister_driver-removeadriverfromtheuartcorelayer
*@drv:lowleveldriverstructure
*
*Removeallreferencestoadriverfromthecoredriver.Thelow
*leveldrivermusthaveremovedallitsportsviathe
*uart_remove_one_port()ifitregisteredthemwithuart_add_one_port().
*(ie,drv->port==NULL)
*/
voiduart_unregister_driver(structuart_driver*drv)
{
structtty_driver*p=drv->tty_driver;
unsignedinti;
/*獲取與該驅動程序關聯的tty_driver實例*/
tty_unregister_driver(p);

/*取消注冊驅動程序,將它與系統中的tty設備斷開關聯*/
put_tty_driver(p);

/*釋放該tty_driver實例,如果此時該實例的使用計數為零,即沒有其他模塊在使用該實例,那么它將會被完全卸載并釋放所有內存資源*/
for(i=0;inr;i++)
tty_port_destroy(&drv->state[i].port);
kfree(drv->state);
drv->state=NULL;
drv->tty_driver=NULL;
}

uart_add_one_port

uart_add_one_port用于將一個UART端口添加到UART驅動程序的狀態表中,并注冊TTY端口設備,讓用戶空間能夠通過該設備與UART通信。

/**
*uart_add_one_port-attachadriver-definedportstructure
*@drv:pointertotheuartlowleveldriverstructureforthisport
*@uport:uartportstructuretouseforthisport.
*
*Thisallowsthedrivertoregisteritsownuart_portstructure
*withthecoredriver.Themainpurposeistoallowthelow
*leveluartdriverstoexpanduart_port,ratherthanhavingyet
*morelevelsofstructures.
*/
intuart_add_one_port(structuart_driver*drv,structuart_port*uport)
{
structuart_state*state;
structtty_port*port;
intret=0;
structdevice*tty_dev;
intnum_groups;

/*檢查是否在中斷上下文中,如果是則直接返回錯誤*/
BUG_ON(in_interrupt());

/*檢查所添加的端口是否超出驅動程序支持的范圍,如果是則返回EINVAL*/
if(uport->line>=drv->nr)
return-EINVAL;

/*獲取該端口所對應的狀態信息(uart_state)以及端口(tty_port)*/
state=drv->state+uport->line;
port=&state->port;

mutex_lock(&port_mutex);
mutex_lock(&port->mutex);

/*檢查端口是否已經被其他設備占用,如果是則返回EINVAL*/
if(state->uart_port){
ret=-EINVAL;
gotoout;
}

/*鏈接端口和驅動程序狀態表,并進行相應的初始化工作,包括PM狀態、控制臺、spinlock等*/
state->uart_port=uport;
uport->state=state;

state->pm_state=UART_PM_STATE_UNDEFINED;
uport->cons=drv->cons;
uport->minor=drv->tty_driver->minor_start+uport->line;

/*
*Ifthisportisaconsole,thenthespinlockisalready
*initialised.
*/
if(!(uart_console(uport)&&(uport->cons->flags&CON_ENABLED))){
spin_lock_init(&uport->lock);
lockdep_set_class(&uport->lock,&port_lock_key);
}
if(uport->cons&&uport->dev)
of_console_check(uport->dev->of_node,uport->cons->name,uport->line);

/*配置端口的屬性,例如波特率、數據位、停止位等*/
uart_configure_port(drv,state,uport);

num_groups=2;
if(uport->attr_group)
num_groups++;

/*分配并設置TTY設備屬性組,這些屬性組包括TTY設備通用屬性組和用戶自定義屬性組*/
uport->tty_groups=kcalloc(num_groups,sizeof(*uport->tty_groups),
GFP_KERNEL);
if(!uport->tty_groups){
ret=-ENOMEM;
gotoout;
}
uport->tty_groups[0]=&tty_dev_attr_group;
if(uport->attr_group)
uport->tty_groups[1]=uport->attr_group;

/*注冊TTY端口設備,并將其與tty_driver和tty_port關聯起來*/
tty_dev=tty_port_register_device_attr(port,drv->tty_driver,
uport->line,uport->dev,port,uport->tty_groups);
/*如果注冊成功,將該設備標記為可喚醒*/
if(likely(!IS_ERR(tty_dev))){
device_set_wakeup_capable(tty_dev,1);
}else{
dev_err(uport->dev,"Cannotregisterttydeviceonline%d
",
uport->line);
}

/*
*EnsureUPF_DEADisnotset.
*/
uport->flags&=~UPF_DEAD;

out:
mutex_unlock(&port->mutex);
mutex_unlock(&port_mutex);

returnret;
}

uart_remove_one_port

uart_remove_one_port用于從核心驅動程序中分離(斷開)一個指定的端口結構。

/**
*uart_remove_one_port-detachadriverdefinedportstructure
*@drv:pointertotheuartlowleveldriverstructureforthisport
*@uport:uartportstructureforthisport
*
*Thisunhooks(andhangsup)thespecifiedportstructurefromthe
*coredriver.Nofurthercallswillbemadetothelow-levelcode
*forthisport.
*/
intuart_remove_one_port(structuart_driver*drv,structuart_port*uport)
{
structuart_state*state=drv->state+uport->line;
structtty_port*port=&state->port;
structtty_struct*tty;
intret=0;

/*檢查當前是否處于中斷上下文中*/
BUG_ON(in_interrupt());

/*檢查uart狀態結構中的uart端口指針是否等于傳遞給該函數的uart端口指針,如果不是則打印一條錯誤消息*/
if(state->uart_port!=uport)
dev_alert(uport->dev,"Removingwrongport:%p!=%p
",
state->uart_port,uport);

/*獲取tty端口結構的互斥鎖,該鎖用于防止并發修改端口狀態*/
mutex_lock(&port_mutex);

/*獲取tty端口結構的互斥鎖,然后檢查uart端口指針是否為空。如果為空,則表示當前端口已被刪除。在這種情況下,將返回-EINVAL并解鎖互斥鎖*/
mutex_lock(&port->mutex);
if(!state->uart_port){
mutex_unlock(&port->mutex);
ret=-EINVAL;
gotoout;
}
/*鎖定port->mutex互斥鎖,并將uport->flags設置為UPF_DEAD,表示該端口已經被關閉。之后解鎖port->mutex。*/
uport->flags|=UPF_DEAD;
mutex_unlock(&port->mutex);

/*從tty層中刪除設備*/
tty_unregister_device(drv->tty_driver,uport->line);

/*獲取tty設備對應的tty結構體,并使用tty_vhangup()函數關閉該tty設備的控制終端。最后,使用tty_kref_put()函數釋放tty結構體的引用計數。*/
tty=tty_port_tty_get(port);
if(tty){
tty_vhangup(port->tty);
tty_kref_put(tty);
}

/*如果該端口用作控制臺,則使用unregister_console()函數取消該端口的控制臺注冊*/
if(uart_console(uport))
unregister_console(uport->cons);

/*根據uport->type的值來釋放端口的IO和內存資源,如果uport->type的值為PORT_UNKNOWN,則表示沒有對應的資源需要釋放*/
if(uport->type!=PORT_UNKNOWN)
uport->ops->release_port(uport);
kfree(uport->tty_groups);

/*將uport->type的值設置為PORT_UNKNOWN,表示該端口不再存在。同時將state->uart_port設置為NULL,表示state對應的端口不再與uport相關聯。*/
uport->type=PORT_UNKNOWN;

state->uart_port=NULL;
out:
mutex_unlock(&port_mutex);

returnret;
}

uart_write_wakeup

uart_write_wakeupuart_write_wakeup喚醒上層因向串口端口寫數據而阻塞的進程,通常在串口發送中斷處理函數中調用該函數。

/*
*Thisroutineisusedbytheinterrupthandlertoscheduleprocessingin
*thesoftwareinterruptportionofthedriver.
*/
voiduart_write_wakeup(structuart_port*port)
{
structuart_state*state=port->state;
/*
*Thismeansyoucalledthisfunction_after_theportwas
*closed.Nocookieforyou.
*/
BUG_ON(!state);
/*函數喚醒與state->port相關聯的終端。*/
tty_wakeup(state->port.tty);
}

uart_suspend_port

uart_suspend_port函數用于將端口掛起以進行電源管理。它執行一系列操作,包括檢查子設備是否可以喚醒系統,停止發送和接收數據,等待發送緩沖區為空,關閉端口,停止控制臺,并更改端口的電源管理狀態。

intuart_suspend_port(structuart_driver*drv,structuart_port*uport)
{
structuart_state*state=drv->state+uport->line;
structtty_port*port=&state->port;
structdevice*tty_dev;
structuart_matchmatch={uport,drv};

/*給port加鎖,以確保在執行其他操作時不會發生競爭條件*/
mutex_lock(&port->mutex);

/*查找與uport->dev相關聯的子設備。它使用match結構體和serial_match_port函數來匹配子設備*/
tty_dev=device_find_child(uport->dev,&match,serial_match_port);
/*如果找到了子設備并且該設備可以喚醒系統,則將uport->irq設置為喚醒中斷,并將uport->irq_wake設置為1。然后,釋放tty_dev并解鎖port的互斥鎖,并返回0*/
if(device_may_wakeup(tty_dev)){
if(!enable_irq_wake(uport->irq))
uport->irq_wake=1;
put_device(tty_dev);
mutex_unlock(&port->mutex);
return0;
}
/*如果找到了子設備但該設備不能喚醒系統,則釋放tty_dev*/
put_device(tty_dev);

/*Nothingtodoiftheconsoleisnotsuspending*/
/*如果控制臺未啟用掛起并且uport是控制臺,則跳轉到unlock解鎖*/
if(!console_suspend_enabled&&uart_console(uport))
gotounlock;

/*將uport->suspended設置為1,表示端口已掛起。*/
uport->suspended=1;

/*如果端口已初始化,則執行一些操作以停止傳輸并關閉端口。這些操作包括設置ASYNCB_SUSPENDED和清除ASYNCB_INITIALIZED標志,停止發送和接收數據,等待發送緩沖區為空,關閉端口*/
if(port->flags&ASYNC_INITIALIZED){
conststructuart_ops*ops=uport->ops;
inttries;

set_bit(ASYNCB_SUSPENDED,&port->flags);
clear_bit(ASYNCB_INITIALIZED,&port->flags);

spin_lock_irq(&uport->lock);
ops->stop_tx(uport);
ops->set_mctrl(uport,0);
ops->stop_rx(uport);
spin_unlock_irq(&uport->lock);

/*
*Waitforthetransmittertoempty.
*/
for(tries=3;!ops->tx_empty(uport)&&tries;tries--)
msleep(10);
if(!tries)
dev_err(uport->dev,"%s%d:Unabletodraintransmitter
",
drv->dev_name,
drv->tty_driver->name_base+uport->line);

ops->shutdown(uport);
}

/*
*Disabletheconsoledevicebeforesuspending.
*/
/**/
/*如果uport是控制臺,則停止控制臺*/
if(uart_console(uport))
console_stop(uport->cons);
/*調用uart_change_pm函數以更改端口的電源管理狀態為UART_PM_STATE_OFF*/
uart_change_pm(state,UART_PM_STATE_OFF);
unlock:
mutex_unlock(&port->mutex);

return0;
}

uart_resume_port

uart_resume_port作用是恢復一個已經掛起的UART端口。

intuart_resume_port(structuart_driver*drv,structuart_port*uport)
{
structuart_state*state=drv->state+uport->line;
structtty_port*port=&state->port;
structdevice*tty_dev;
structuart_matchmatch={uport,drv};
structktermiostermios;

mutex_lock(&port->mutex);

/*使用device_find_child搜索與名為match的structuart_match匹配的uport->dev的子設備*/
tty_dev=device_find_child(uport->dev,&match,serial_match_port);

/*如果找到設備并且端口未掛起并且設備可以喚醒,則函數禁用IRQ喚醒并返回0*/
if(!uport->suspended&&device_may_wakeup(tty_dev)){
if(uport->irq_wake){
disable_irq_wake(uport->irq);
uport->irq_wake=0;
}
put_device(tty_dev);
mutex_unlock(&port->mutex);
return0;
}

/*函數將uport->suspended設置為0*/
put_device(tty_dev);
uport->suspended=0;

/*
*Re-enabletheconsoledeviceaftersuspending.
*/
/*如果端口是控制臺端口,則函數將termios結構設置為控制臺cflag設置*/
if(uart_console(uport)){
/*
*Firsttrytousetheconsolecflagsetting.
*/
memset(&termios,0,sizeof(structktermios));
termios.c_cflag=uport->cons->cflag;

/*
*Ifthat'sunset,usethettytermiossetting.
*/
if(port->tty&&termios.c_cflag==0)
termios=port->tty->termios;
/*如果啟用了控制臺掛起,則函數使用uart_change_pm將電源管理狀態更改為打開狀態,使用uport->ops->set_termios設置termios,并使用console_start啟動控制臺*/
if(console_suspend_enabled)
uart_change_pm(state,UART_PM_STATE_ON);
uport->ops->set_termios(uport,&termios,NULL);
if(console_suspend_enabled)
console_start(uport->cons);
}

if(port->flags&ASYNC_SUSPENDED){
conststructuart_ops*ops=uport->ops;
intret;
/*如果端口已掛起,則函數使用uart_change_pm將電源管理狀態更改為打開狀態*/
uart_change_pm(state,UART_PM_STATE_ON);
spin_lock_irq(&uport->lock);
/*使用ops->set_mctrl將調制解調器控制線設置為0*/
ops->set_mctrl(uport,0);
spin_unlock_irq(&uport->lock);
if(console_suspend_enabled||!uart_console(uport)){
/*Protectedbyportmutexfornow*/
structtty_struct*tty=port->tty;
/*使用ops->startup啟動端口*/
ret=ops->startup(uport);
if(ret==0){
/*如果端口成功啟動,則使用uart_change_speed更改端口速度,使用ops->start_tx啟動傳輸,并在port->flags中設置ASYNCB_INITIALIZED位*/
if(tty)
uart_change_speed(tty,state,NULL);
spin_lock_irq(&uport->lock);
ops->set_mctrl(uport,uport->mctrl);

ops->start_tx(uport);
spin_unlock_irq(&uport->lock);
set_bit(ASYNCB_INITIALIZED,&port->flags);
}else{
/*
*Failedtoresume-maybehardwarewentaway?
*Clearthe"initialized"flagsowewon'ttry
*tocallthelowleveldriversshutdownmethod.
*/
/*如果端口無法恢復,則函數清除ASYNCB_INITIALIZED位并調用uart_shutdown*/
uart_shutdown(tty,state);
}
}

clear_bit(ASYNCB_SUSPENDED,&port->flags);
}

mutex_unlock(&port->mutex);

return0;
}

uart_get_baud_rate

uart_get_baud_rate,該函數的作用是根據給定的終端設置和范圍,獲取一個可用的波特率。如果無法獲取滿足要求的波特率,則會盡可能地使用最接近的波特率。

/**
*uart_get_baud_rate-returnbaudrateforaparticularport
*@port:uart_portstructuredescribingtheportinquestion.
*@termios:desiredtermiossettings.
*@old:oldtermios(orNULL)
*@min:minimumacceptablebaudrate
*@max:maximumacceptablebaudrate
*
*Decodethetermiosstructureintoanumericbaudrate,
*takingaccountofthemagic38400baudrate(withspd_*
*flags),andmappingthe%B0rateto9600baud.
*
*Ifthenewbaudrateisinvalid,trytheoldtermiossetting.
*Ifit'sstillinvalid,wetry9600baud.
*
*Updatethe@termiosstructuretoreflectthebaudrate
*we'reactuallygoingtobeusing.Don'tdothisforthecase
*whereB0isrequested("hangup").
*/
unsignedint
uart_get_baud_rate(structuart_port*port,structktermios*termios,
structktermios*old,unsignedintmin,unsignedintmax)
{
unsignedinttry;
unsignedintbaud;
unsignedintaltbaud;
inthung_up=0;
upf_tflags=port->flags&UPF_SPD_MASK;

switch(flags){
caseUPF_SPD_HI:
altbaud=57600;
break;
caseUPF_SPD_VHI:
altbaud=115200;
break;
caseUPF_SPD_SHI:
altbaud=230400;
break;
caseUPF_SPD_WARP:
altbaud=460800;
break;
default:
altbaud=38400;
break;
}

for(try=0;try=min&&baud<=?max)
???return?baud;

??/*
???*?Oops,?the?quotient?was?zero.??Try?again?with
???*?the?old?baud?rate?if?possible.
???*/
??termios->c_cflag&=~CBAUD;
if(old){
baud=tty_termios_baud_rate(old);
if(!hung_up)
tty_termios_encode_baud_rate(termios,
baud,baud);
old=NULL;
continue;
}

/*
*Asalastresort,iftherangecannotbemetthenclipto
*thenearestchipsupportedrate.
*/
if(!hung_up){
if(baud<=?min)
????tty_termios_encode_baud_rate(termios,
???????min?+?1,?min?+?1);
???else
????tty_termios_encode_baud_rate(termios,
???????max?-?1,?max?-?1);
??}
?}
?/*?Should?never?happen?*/
?WARN_ON(1);
?return?0;
}

該函數所作工作如下

根據 UPF_SPD_MASK 標志位解析出一個備用波特率 altbaud。

函數會嘗試兩次獲取波特率。第一次,函數會從 termios 中解析出當前波特率,如果它等于 38400,則將波特率設置為備用波特率 altbaud。如果波特率等于 0(即請求“掛起”),則設置波特率為 9600。如果波特率在 min 和 max 范圍內,則返回該波特率。

如果第一次獲取的波特率為 0,則函數會嘗試使用舊的終端設置。

如果仍然無法滿足要求,函數會將波特率剪裁到最接近的支持的波特率。剪裁的方式是,如果波特率小于等于最小值 min,則設置波特率為 min + 1,否則設置波特率為 max - 1。

uart_get_divisor

uart_get_divisor,用于計算給定端口的 UART 時鐘分頻器值,以實現指定的波特率 。主要是通過一些基本的數學運算來計算出 UART 時鐘分頻器值。這個值是用來配置 UART 硬件的,以實現指定的波特率。

在串口通信中,時鐘分頻器值對應著波特率,即時鐘分頻器值越小,波特率越高,傳輸速度越快。

/**
*uart_get_divisor-returnuartclockdivisor
*@port:uart_portstructuredescribingtheport.
*@baud:desiredbaudrate
*
*Calculatetheuartclockdivisorfortheport.
*/
unsignedint
uart_get_divisor(structuart_port*port,unsignedintbaud)
{
unsignedintquot;

/*
*Oldcustomspeedhandling.
*/
if(baud==38400&&(port->flags&UPF_SPD_MASK)==UPF_SPD_CUST)
quot=port->custom_divisor;
else
quot=DIV_ROUND_CLOSEST(port->uartclk,16*baud);

returnquot;
}

該函數所作工作如下

首先根據給定的波特率計算出 UART 時鐘周期的長度 period,這個周期的長度是通過 16 * baud 計算得到的。然后,將端口的 UART 時鐘頻率除以 period,得到的商值 quot 就是需要的 UART 時鐘分頻器值。這里使用了 DIV_ROUND_CLOSEST 宏,它的作用是將浮點數四舍五入為最接近的整數值。

在計算時鐘分頻器值時,還有一個特殊的情況需要處理。如果給定的波特率為 38400,并且端口的標志位值為 UPF_SPD_CUST,則需要使用端口的自定義分頻器值,而不是根據公式計算出來的值。這是因為在一些老的串口驅動中,可能會使用自定義分頻器值來支持一些特殊的波特率。

uart_update_timeout

uart_update_timeout用于設置串口的 FIFO 超時時間。FIFO(First-In-First-Out)是串口硬件中用于緩存數據的一種常見結構,它可以提高串口傳輸的效率。而超時時間則是指在 FIFO 中沒有數據傳輸時,等待多長時間后自動清空 FIFO。超時時間的設置可以影響串口傳輸的穩定性和效率。

/**
*uart_update_timeout-updateper-portFIFOtimeout.
*@port:uart_portstructuredescribingtheport
*@cflag:termioscflagvalue
*@baud:speedoftheport
*
*SettheportFIFOtimeoutvalue.The@cflagvalueshould
*reflecttheactualhardwaresettings.
*/
void
uart_update_timeout(structuart_port*port,unsignedintcflag,
unsignedintbaud)
{
unsignedintbits;

/*bytesizeandparity*/
switch(cflag&CSIZE){
caseCS5:
bits=7;
break;
caseCS6:
bits=8;
break;
caseCS7:
bits=9;
break;
default:
bits=10;
break;/*CS8*/
}

if(cflag&CSTOPB)
bits++;
if(cflag&PARENB)
bits++;

/*
*Thetotalnumberofbitstobetransmittedinthefifo.
*/
bits=bits*port->fifosize;

/*
*Figurethetimeouttosendtheabovenumberofbits.
*Add.02secondsofslop
*/
port->timeout=(HZ*bits)/baud+HZ/50;
}

根據終端設置中的 cflag 值,計算出每個字節需要傳輸的位數 bits。根據 cflag 中的 CSIZE 標志位,確定每個字節的位數(5、6、7 或 8 位),并根據 CSTOPB 和 PARENB 標志位,增加停止位和奇偶校驗位的位數。

將每個字節需要傳輸的位數 bits 乘以 FIFO 的大小,得到總共需要傳輸的位數。

根據波特率和總共需要傳輸的位數,計算出超時時間。將總共需要傳輸的位數除以波特率,得到傳輸這些數據所需要的時間,再加上一些額外的時間(0.02 秒)作為緩沖,得到超時時間。

最后,將計算出來的超時時間賦值給端口結構體中的 timeout 成員變量,從而完成 FIFO 超時時間的設置。

uart_match_port

uart_match_port根據兩個端口的屬性比較兩個串口端口是否相等。

/*
*Arethetwoportsequivalent?
*/
intuart_match_port(structuart_port*port1,structuart_port*port2)
{
if(port1->iotype!=port2->iotype)
return0;

switch(port1->iotype){
caseUPIO_PORT:
return(port1->iobase==port2->iobase);
caseUPIO_HUB6:
return(port1->iobase==port2->iobase)&&
(port1->hub6==port2->hub6);
caseUPIO_MEM:
caseUPIO_MEM32:
caseUPIO_MEM32BE:
caseUPIO_AU:
caseUPIO_TSI:
return(port1->mapbase==port2->mapbase);
}
return0;
}

根據兩個串口端口的 iotype 屬性進行比較,如果不相等,則兩個端口不相等,函數返回 0。

根據 iotype 屬性的不同,比較兩個端口的其他屬性。對于 UPIO_PORT 和 UPIO_HUB6 類型的端口,比較它們的 iobase 和 hub6 屬性是否相等;對于其他類型的端口,比較它們的 mapbase 屬性是否相等。如果所有屬性都相等,則兩個端口相等,函數返回 1,否則返回 0。

uart_console_write

uart_console_write用于將控制臺消息寫入串口。

嵌入式系統中,通常需要將控制臺輸出重定向到串口,以便進行調試和日志記錄。該函數實現了將一個字符串寫入串口的操作,其中需要將字符串中的換行符轉換為回車換行符。

/**
*uart_console_write-writeaconsolemessagetoaserialport
*@port:theporttowritethemessage
*@s:arrayofcharacters
*@count:numberofcharactersinstringtowrite
*@putchar:functiontowritecharactertoport
*/
voiduart_console_write(structuart_port*port,constchar*s,
unsignedintcount,
void(*putchar)(structuart_port*,int))
{
unsignedinti;

for(i=0;i

該函數的實現主要是遍歷字符串中的所有字符,并將每個字符寫入串口。在寫入字符之前,需要判斷該字符是否為換行符。如果是換行符,則需要先將其轉換為回車換行符,再寫入串口。

總結

對接底層的部分,Kernel 主要是提供了兩個接口:

1、uart_register_driver (一次調用)

2、uart_add_one_port (多次調用)

通過這兩個接口,實現了芯片將自己的 UART 對接到 Linux Kernel UART Driver 中。

芯片廠商需要自行設計并實現的部分有:

1、uart_drvier 結構(一個)

2、uart_port 結構(多個)

3、uart_ops 對串口的操作集(可能一個,可能多個)

所以從結構上來看,整個對接過程為:

71ea8d618cd911e8ca713408e39c1bbd.png
b1f23b238e2e3853a9dab36cc3dcd8ca.png

這里有一點需要特別注意,在對接底層的部分中,Kernel 定義了一個結構體叫:struct uart_ops

在 tty 層,對 tty_driver 初始化的時候(serial_core.c),調用到:

tty_set_operations(normal,&uart_ops);

而他的實現是:

voidtty_set_operations(structtty_driver*driver,conststructtty_operations*op)

{
driver->ops=op;
};
EXPORT_SYMBOL(tty_set_operations);

看到了么,傳進去的是 ****tty_operations *op****,所以,在 tty_driver 掛接的 uart_ops 并非那個 struct uart_ops,而是這個 serial_core.c 文件內定義的:

staticconststructtty_operationsuart_ops={
.open=uart_open,
.close=uart_close,
.write=uart_write,
.put_char=uart_put_char,
.flush_chars=uart_flush_chars,
.write_room=uart_write_room,
.chars_in_buffer=uart_chars_in_buffer,
.flush_buffer=uart_flush_buffer,
.ioctl=uart_ioctl,
.throttle=uart_throttle,
.unthrottle=uart_unthrottle,
.send_xchar=uart_send_xchar,
.set_termios=uart_set_termios,
.set_ldisc=uart_set_ldisc,
.stop=uart_stop,
.start=uart_start,
.hangup=uart_hangup,
.break_ctl=uart_break_ctl,
.wait_until_sent=uart_wait_until_sent,
#ifdefCONFIG_PROC_FS
.proc_show=uart_proc_show,
#endif
.tiocmget=uart_tiocmget,
.tiocmset=uart_tiocmset,
.set_serial=uart_set_info_user,
.get_serial=uart_get_info_user,
.get_icount=uart_get_icount,
#ifdefCONFIG_CONSOLE_POLL
.poll_init=uart_poll_init,
.poll_get_char=uart_poll_get_char,
.poll_put_char=uart_poll_put_char,
#endif
};

名字一樣,但是不是同一個結構,容易讓人眼花~~

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • uart
    +關注

    關注

    22

    文章

    1235

    瀏覽量

    101354
  • 數據結構
    +關注

    關注

    3

    文章

    573

    瀏覽量

    40123
  • 串口驅動
    +關注

    關注

    2

    文章

    82

    瀏覽量

    18647

原文標題:【驅動】串口驅動分析(三)-serial driver

文章出處:【微信號:嵌入式與Linux那些事,微信公眾號:嵌入式與Linux那些事】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    【開源的串口可視化工具——Serial Studio】

    分享一個開源的串口項目——Serial Studio,這是一個強大的數據可視化軟件,支持串口通信,串口終端,網絡通信 TCP/UDP,MQTT通信協議。這個項目遵循MIT協議,所以是可
    的頭像 發表于 01-18 15:03 ?1.2w次閱讀
    【開源的<b class='flag-5'>串口</b>可視化工具——<b class='flag-5'>Serial</b> Studio】

    串口設備框架serial_v2源碼分析-阻塞模式

    serial_v2中,串口設備以應用層視角,即阻塞模式或非阻塞模式來作為該串口設備的開啟標志.
    的頭像 發表于 09-14 11:34 ?2079次閱讀

    USB To Serial Driver

    USB To Serial Driver  
    發表于 09-28 15:57

    關于串口調試助手和virtual serial port driver

    想問下proteus做單片機和pc通信提示需要virtual serial port driver串口調試助手,想問下怎么用,下載下來的這兩個好像不起作用
    發表于 11-17 16:55

    請問driver/tty/serial/8250/8250-omap.c驅動是什么串口驅動

    是CONFIG_SERIAL_8250,而不是CONFIG_SERIAL_OMAP,那么這里為什么要配置成8250呢?8250的串口驅動是干嘛用的?
    發表于 06-21 02:01

    什么是串口驅動器?

    什么是串口驅動器?串口驅動的作用?以及它如何適用于GSM M66模塊。以上來自于谷歌翻譯以下為原文 what is serial
    發表于 01-14 15:24

    虛擬串口Virtual Serial Port Driver使用報錯怎么解決?

    使用虛擬串口到底是干什么?虛擬串口Virtual Serial Port Driver使用報錯怎么解決?
    發表于 02-22 06:57

    Open Universal Serial Bus Driv

    Open Universal Serial Bus Driver Interface (OpenUSBDI) Specification This document specifies
    發表于 04-11 19:16 ?15次下載

    Serial Monitor (串口監視、檢測、分析工具)v

    Serial Monitor:Serial Monitor是一款功能強大的串口監視、檢測、分析工具,軟件使用更加簡單,尤其適合開發人員使用。 
    發表于 05-26 09:03 ?86次下載

    Proteus串口資料COMPIM Serial Port

    Proteus串口資料COMPIM Serial Port Model The COMPIM model is a Physical Interface Model of a serial port. Incoming
    發表于 04-17 16:13 ?0次下載

    Virtual Serial Port Driver 6.9(虛擬串口)

    電子發燒友網站提供《Virtual Serial Port Driver 6.9(虛擬串口).rar》資料免費下載
    發表于 08-02 00:00 ?39次下載

    經典實用USB轉串口驅動STM32 Virtual COM Port Driver(V1.3.1)

    電子發燒友網站提供《經典實用USB轉串口驅動STM32 Virtual COM Port Driver(V1.3.1).zip》資料免費下載
    發表于 08-01 10:32 ?1289次下載

    matlab中的串口軟件serial_1

    電子發燒友網站提供《matlab中的串口軟件serial_1.zip》資料免費下載
    發表于 07-07 16:32 ?1次下載

    PL-2303 Vista Driver Installer12

    PL-2303 Vista Driver Installer串口驅動
    發表于 12-09 15:45 ?1次下載

    NodeMCU V3.0 Arduino開發串口使用

    NodeMCU V3.0 Arduino開發串口使用串口使用串口使用void setup() { // put your setup code here, to run once
    發表于 11-16 09:51 ?1次下載
    NodeMCU V3.0 Arduino開發<b class='flag-5'>之</b><b class='flag-5'>串口</b>使用
    主站蜘蛛池模板: 日本午夜精品理论片A级APP发布| 帝王被大臣们调教高肉| 免费在线视频成人| 99久久精品国产交换| 欧美特级特黄AAAAA片| 第一福利在线永久视频| 无码人妻99久久密AV| 国产亚洲精品久久无码98| 亚洲精品成人无码区一在线观看| 精品久久久麻豆国产精品| 真实国产乱子伦精品一区二区三区| 免费看男人J放进女人J无遮掩| GAY2022空少被体育生暴菊| 日日噜噜夜夜狠狠视频| 国产免费内射又粗又爽密桃视频| 91进入蜜桃臀在线播放| 国产精品久久久久影院| 亚洲精品无码葡京AV天堂| 久久女婷五月综合色啪| blacked黑人战小美女| 三叶草未满十八岁| 国产亚洲精品久久无亚洲| 中文字幕在线视频在线看| 琪琪电影午夜理论片YY6080| 国产浮力草草影院CCYY| 亚洲欧美中文字幕高清在线| 美艳人妻在厨房翘着屁股| 嘟嘟嘟WWW免费高清在线中文| 亚洲mv在线观看| 麻豆精品无码久久久久久久久| 超碰国产人人做人人爽| 亚洲AV精品无码喷水直播间| 旧里番6080在线观看| 成人免费视频在线观看| 亚洲精品高清中文字幕完整版| 妈妈的职业3完整版在线播放| 抽插妇女疯狂视频| 亚洲综合色婷婷在线影院| 欧美高清vivoesosexo18| 国产日韩精品SUV| 9420高清免费观看在线大全|