linux内核网络驱动框架(linux驱动开发篇)

这篇具有很好参考价值的文章主要介绍了linux内核网络驱动框架(linux驱动开发篇)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  • 网络驱动的核心:
    1、就是初始化 net_device 结构体中的各个成员变量,
    2、然后将初始化完成以后的 net_device 注册到 Linux 内核中

1、网络设备(用net_device结构体)

/*
@	net_device 结构体 
*/
  struct net_device { 
      char                  name[IFNAMSIZ]; /*网络设备名字*/
      struct hlist_node     name_hlist; 
      char                  *ifalias; 
      
      unsigned long        mem_end;/*共享内存结束地址*/
      unsigned long       mem_start;/*共享内存起始地址*/
      unsigned long         base_addr; /*网络设备IO地址*/
      int               irq; /*网络设备的中断号*/
   
      atomic_t            carrier_changes; 
     unsigned long         state; 
  
     struct list_head      dev_list; /*全局网络设备列表*/
     struct list_head      napi_list;/*napi机制==napi设备的列表入口*/ 
     struct list_head      unreg_list;/*注销网络设备的列表入口*/ 
     struct list_head      close_list; /*关闭的网络设备列表入口*/
... 
     const struct net_device_ops *netdev_ops; /*网络设备的操作集函数*/
     const struct ethtool_ops   *ethtool_ops; /*网络管理工具相关函数集*/
 #ifdef CONFIG_NET_SWITCHDEV 
     const struct swdev_ops     *swdev_ops; 
 #endif 
  
     const struct header_ops     *header_ops; /*头部的相关操作函数集,比如创建、解析、缓冲等*/
  
     unsigned int          flags; /*网络接口标志*/
...  
     unsigned char         if_port; /*指定接口的端口类型*/
     unsigned char         dma; /*网络设备所使用的DMA通道*/
  
     unsigned int          mtu; /*网络最大传输单元*/
     unsigned short        type;/*指定 ARP模块的类型*/ 
     unsigned short        hard_header_len; 
  
     unsigned short        needed_headroom; 
     unsigned short        needed_tailroom; 
  
     /* Interface address info. */ 
     unsigned char         perm_addr[MAX_ADDR_LEN]; /*永久的硬件地址*/
     unsigned char         addr_assign_type; 
     unsigned char         addr_len; /*硬件地址长度。*/
... 
 /* 
  * Cache lines mostly used on receive path (including  
eth_type_trans()) 
  */ 
     unsigned long         last_rx; /*最后接收的数据包时间戳,记录的是jiffies*/
  
     /* Interface address info used in eth_type_trans() */ 
     unsigned char         *dev_addr; /*硬件地址,是当前分配的MAC 地址*/
 
 #ifdef CONFIG_SYSFS 
      struct netdev_rx_queue  *_rx; /*接收队列*/
  
     unsigned int          num_rx_queues; /*接收队列数量*/
     unsigned int          real_num_rx_queues; /*当前活动的队列数量*/
  
 #endif 
... 
 /* 
  * Cache lines mostly used on transmit path 
  */ 
     struct netdev_queue  *_tx  /*发送队列*/ ____cacheline_aligned_in_smp; 
     unsigned int          num_tx_queues; /*发送队列数量*/
     unsigned int          real_num_tx_queues; /*当前有效的发送队列数量。*/
     struct Qdisc          *qdisc; 
     unsigned long         tx_queue_len; 
     spinlock_t          tx_global_lock; 
     int               watchdog_timeo; 
... 
     /* These may be needed for future network-power-down code. *
  
     /* 
      * trans_start here is expensive for high speed devices on S
      * please use netdev_queue->trans_start instead. 
      */ 
     unsigned long         trans_start; /*最后的数据包发送的时间戳,记录的是jiffies*/
... 
     struct phy_device   *phydev; /*对应的PHY 设备*/
     struct lock_class_key *qdisc_tx_busylock; 
 }; 


/*
@	申请net_device ===11111111111111111111111111111111
*/
#define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \ 
   alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, 1, 1) 

/*可以看出alloc_netdev的本质是alloc_netdev_mqs函数*/

/*
@	sizeof_priv:私有数据块大小。 
@	name:设备名字
@	setup:回调函数,初始化设备的设备后调用此函数
@	txqs:分配的发送队列数量
@	rxqs:分配的接收队列数量
@	如果申请成功的话就返回申请到的 net_device指针,失败的话就返回NULL。
*/
struct net_device  * alloc_netdev_mqs ( int      sizeof_priv,   const char    *name,   
                  void      (*setup) (struct net_device *)) 
                  unsigned int    txqs,   
                  unsigned int    rxqs); 

/*事实上网络设备有多种,大家不要以为就只有以太网一种,比如光纤分布式数据接口(FDDI)、以太网设备(Ethernet)、红外数据接口(InDA)、高性能并行接口(HPPI)、CAN 网络等*/


/*
@	本章讲解的以太网,针对以太网封装的net_device申请函数是alloc_etherdev,这也是一个宏
@	alloc_etherdev 最终依靠的是 alloc_etherdev_mqs 函数
*/
 #define alloc_etherdev(sizeof_priv) alloc_etherdev_mq(sizeof_priv, 1) 
 #define alloc_etherdev_mq(sizeof_priv, count) alloc_etherdev_mqs(sizeof_priv, count, count) 

/*
@	alloc_etherdev 最终依靠的是 alloc_etherdev_mqs 函数
*/
 struct net_device *alloc_etherdev_mqs(int sizeof_priv, unsigned int txqs, unsigned int rxqs) 
 { 
	return alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN,ether_setup, txqs, rxqs); /*申请net_device,*/
 } 

/*
@	对是申请的设备进行初始化
@	ether_setup函数会对 net_device做初步的初始化
*/
 void ether_setup(struct net_device *dev) 
 { 
    dev->header_ops       = &eth_header_ops; 
    dev->type           = ARPHRD_ETHER; 
    dev->hard_header_len = ETH_HLEN; 
    dev->mtu              = ETH_DATA_LEN; 
    dev->addr_len         = ETH_ALEN; 
    dev->tx_queue_len     = 1000; /* Ethernet wants good queues *
    dev->flags          = IFF_BROADCAST|IFF_MULTICAST; 
    dev->priv_flags       |= IFF_TX_SKB_SHARING; 
  
    eth_broadcast_addr(dev->broadcast); 
 } 


/*
@	删除net_device(net_device)=====22222222222222222222222222222222222222222
@	注销网络驱动的时候需要释放掉前面已经申请到的 net_device
@	dev:要释放掉的net_device指针。 
@	返回值:无
*/
void free_netdev(struct net_device *dev) 


/*
@	注册net_device ==33333333333333333333333333333333333333
@	net_device申请并初始化完成以后就需要向内核注册net_device,要用到函数register_netdev
@	dev:要注册的net_device指针
@	返回值:0 注册成功,负值 注册失败。
*/
int register_netdev(struct net_device *dev) 


/*
@	注销net_device ==44444444444444444444444444444444444
@	dev:要注销的net_device指针
@	返回值:无
*/
void unregister_netdev(struct net_device *dev) 

2、网络设备的操作集( net_device_ops结构体 )

/*
@	定义在 include/linux/netdevice.h 文件中
@	net_device_ops结构体里面都是一些以“ndo_”开头的函数,这些函数就需要网络驱动编写人员去实现
@	net_device_ops 结构体 
*/
  struct net_device_ops { 
      int         (*ndo_init)(struct net_device *dev); /*当第一次注册网络设备的时候此函数会执行*/
      void        (*ndo_uninit)(struct net_device *dev); /*卸载网络设备的时候此函数会执行*/
      int         (*ndo_open)(struct net_device *dev); /*打开网络设备的时候此函数会执行,网络驱动程序需要实现此函数,非常重要*/
      int         (*ndo_stop)(struct net_device *dev); 
      netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb, 
                             struct net_device *dev); /*当需要发送数据的时候此函数就会执行,此函数有一个参数为 sk_buff结构体指针,sk_buff结构体在Linux的网络驱动中非常重要,sk_buff保存了上层传递给网络驱动层的数据。也就是说,要发送出去的数据都存在了sk_buff中*/
      u16         (*ndo_select_queue)(struct net_device *dev, struct sk_buff *skb, void *accel_priv,select_queue_fallback_t fallback); /*设备支持多传输队列的时候选择使用哪个队列*/
      void       (*ndo_change_rx_flags)(struct net_device *dev, 
                                 int flags); 
      void        (*ndo_set_rx_mode)(struct net_device *dev);/*此函数用于改变地址过滤列表*/ 
      int         (*ndo_set_mac_address)(struct net_device *dev,  void *addr); /*此函数用于修改网卡的 MAC 地址*/
      int         (*ndo_validate_addr)(struct net_device *dev); /*验证 MAC 地址是否合法,*/
      int         (*ndo_do_ioctl)(struct net_device *dev, 
                              struct ifreq *ifr, int cmd); /*用户程序调用 ioctl 的时候此函数就会执行,*/
      int         (*ndo_set_config)(struct net_device *dev, 
                                struct ifmap *map); 
      int         (*ndo_change_mtu)(struct net_device *dev, 
                            int new_mtu); /*更改MTU大小。*/
      int         (*ndo_neigh_setup)(struct net_device *dev, 
                                  struct neigh_parms *); 
     void       (*ndo_tx_timeout) (struct net_device *dev); /*当发送超时的时候产生会执行,一般都是网络出问题了导
致发送超*/
... 
 #ifdef CONFIG_NET_POLL_CONTROLLER 
     void       (*ndo_poll_controller)(struct net_device *dev); /*使用查询方式来处理网卡数据的收发。*/
     int         (*ndo_netpoll_setup)(struct net_device *dev, 
                              struct netpoll_info *info); 
     void       (*ndo_netpoll_cleanup)(struct net_device *dev); 
 #endif 
... 
     int         (*ndo_set_features)(struct net_device *dev, etdev_features_t features); /*修改net_device的 features属性,设置相应的硬件属性*/
... 
 }; 

/*
@	:ndo_open 函数,:ndo_stop 函数==以NXP的I.MX 系列SOC 网络驱动为例,会在此函数中做如下工作
*/
·使能网络外设时钟。 
·申请网络所使用的环形缓冲区。 
·初始化MAC 外设。 
·绑定接口对应的 PHY。 
·如果使用NAPI 的话要使能NAPI模块,通过 napi_enable函数来使能。 
·开启PHY。 
·调用netif_tx_start_all_queues来使能传输队列,也可能调用netif_start_queue函数。 
·…… 

do_stop 函数,关闭网络设备的时候此函数会执行,网络驱动程序也需要实现此
函数。以NXP的I.MX 系列SOC 网络驱动为例,会在此函数中做如下工作: 
·停止PHY。 
·停止NAPI功能。 
·停止发送功能。 
·关闭MAC。 
·断开PHY 连接。 
·关闭网络时钟。 
·释放数据缓冲区。 
  ·…… 

3、sk_buff结构体

  • 网络是分层的,对于应用层而言不用关系具体的底层是如何工作的,只需要按照协议将要发送或接收的数据打包好即可
  • 打包好以后都通过dev_queue_xmit函数将数据发送出去,接收数据的话使用netif_rx函数即可
/*
@	定义在 include/linux/netdevice.h 中
@	函数用于将网络数据发送出去
@	skb:	要发送的数据,这是一个sk_buff结构体指针,sk_buff是Linux网络驱动中一个非常重要的结构体,网络数据就是以 sk_buff 保存的,各个协议层在 sk_buff 中添加自己的协议头,最终由底层驱动讲sk_buff中的数据发送出去
@			网络数据的接收过程恰好相反,网络底层驱动将接收到的原始数据打包成 sk_buff,然后发送给上层协议,上层会取掉相应的头部,然后将最终的数据发送给用户
@	返回值:0 发送成功,负值 发送失败。   
*/
static inline int dev_queue_xmit(struct sk_buff    *skb) 


/*
@	上层接收数据的话使用netif_rx函数.但是最原始的网络数据一般是通过轮询、中断或NAPI的方式来接收
@	skb:保存接收数据的 sk_buff
@	返回值:NET_RX_SUCCESS 成功,NET_RX_DROP 数据包丢弃。   
*/
int netif_rx(struct sk_buff *skb) 

  • sk_buff 是 Linux 网络重要的数据结构,用于管理接收或发送数据包
/*
@	定义在 include/linux/skbuff.h 中
@	 sk_buff 结构体 
*/
  struct sk_buff { 
      union { 
          struct { 
              /* These two members must be first. */ 
              struct sk_buff        *next; 
              struct sk_buff        *prev; /*构成一个双向链表*/
   
              union { 
                  ktime_t         tstamp; /*数据包接收时或准备发送时的时间戳*/
                  struct skb_mstamp skb_mstamp; 
              }; 
          }; 
          struct rb_node  rbnode; /* used in netem & tcp stack */ 
      }; 
      struct sock         *sk; /*当前 sk_buff所属的Socket*/
      struct net_device     *dev; /*当前 sk_buff从哪个设备接收到或者发出的*/
   
      char                cb[48] __aligned(8); /*cb 为控制缓冲区,不管哪个层都可以自由使用此缓冲区,用于放置私有数据。 */
   
      unsigned long         _skb_refdst; 
      void                (*destructor)(struct sk_buff *skb); /*当释放缓冲区的时候可以在此函数里面完成某些动作*/
.... 
      unsigned int          len, data_len; /*:len为实际的数据长度*/
      __u16               mac_len, hdr_len; /*:mac_len 为连接层头部长度,也就是 MAC 头的长度*/
.... 
     __be16              protocol; /*protocol协议*/
     __u16               transport_header; /*传输层头部*/
     __u16               network_header; /*网络层头部*/
     __u16               mac_header; /*链接层头部*/
  
     /* private: */ 
     __u32               headers_end[0]; /*缓冲区的尾部*/
     /* public: */ 
  
     /* These elements must be at the end, see alloc_skb() for 
details.  */ 
     sk_buff_data_t      tail; /*实际数据的尾部*/
     sk_buff_data_t        end; /*缓冲区的尾部*/
     unsigned char         *head, *data; /*:head 指向缓冲区的头部,data 指向实际数据的头部*/
     unsigned int          truesize; 
     atomic_t            users; 
 }; 
  • 再上述结构体中的缓冲区的head、end和实际数据区的data、tail
    linux内核网络驱动框架(linux驱动开发篇)
/*===11111111111111111111111111111111111111111111111111
@	分配sk_buff 
@	定义在include/linux/skbuff.h中
@	size:要分配的大小,也就是skb数据段大小
@	priority:为GFP MASK宏,比如GFP_KERNEL、GFP_ATOMIC 等
@	返回值:分配成功的话就返回申请到的 sk_buff首地址,失败的话就返回 NULL
*/
static inline struct sk_buff *alloc_skb(unsigned int    size, gfp_t      priority) 
/*
@	定义在include/linux/skbuff.h中
@	在网络设备驱动中常常使用 netdev_alloc_skb 来为某个设备申请一个用于接收的 skb_buff,
@	dev:要给哪个设备分配 sk_buff。 
@	length:要分配的大小。 
@	返回值:分配成功的话就返回申请到的 sk_buff首地址,失败的话就返回 NULL
*/
static inline struct sk_buff *netdev_alloc_skb(struct net_device   *dev, unsigned int      length) 

/*=======222222222222222222222222222222222222222222222222
@	释放sk_buff 
@	定义在include/linux/skbuff.c中
@	skb:要释放的sk_buff。
@	返回值:无
*/
void kfree_skb(struct sk_buff *skb) 

/*=======33333333333333333333333333333333333333333333333
@	skb_put、skb_push、sbk_pull和 skb_reserve 四个函数用于变更 sk_buff
*/
/*函数用于在尾部扩展 skb_buff的数据区,也就将 skb_buff 的 tail 后移 n 个字节,从而导致 skb_buff 的 len 增加 n 个字节
@	skb:要操作的sk_buff
@	len:要增加多少个字节。
@	返回值:扩展出来的那一段数据区首地址
*/
unsigned char *skb_put(struct sk_buff   *skb, unsigned int   len)   
/*	skb_push函数用于在头部扩展 skb_buff的数据区
@	skb:要操作的sk_buff
@	要增加多少个字节
@	返回值:扩展完成以后新的数据区首地址。
*/
unsigned char *skb_push(struct sk_buff    *skb, unsigned int   len) 
/*	sbk_pull函数用于从 sk_buff的数据区起始位置删除数据
@	skb:要操作的sk_buff
@	len:要删除的字节数。
@	  返回值:删除以后新的数据区首地址
*/
unsigned char *skb_pull(struct sk_buff    *skb, unsigned int   len
/*	sbk_reserve函数用于调整缓冲区的头部大小
@	skb:要操作的sk_buff
@	len:要增加的缓冲区头部大小
@	返回值:无。 
*/
static inline void skb_reserve(struct sk_buff    *skb, int    len) 
  • 为了更好的解释上面四个函数,下面是结构示意图:
    linux内核网络驱动框架(linux驱动开发篇)
    linux内核网络驱动框架(linux驱动开发篇)
    linux内核网络驱动框架(linux驱动开发篇)
    4、网络NAPI处理机制 =====很重要的

  • Linux 里面的网络数据接收也轮询和中断两种,
    1、中断的好处就是响应快,数据量小的时候处理及时,速度、快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发生,消耗大量的CPU处理时间在中断自身处理上

    2、轮询恰好相反,响应没有中断及时,但是在处理大量数据的时候不需要消耗过多的CPU 处理时间。

  • linux在这两个处理方式的基础上,提出了另外一种网络数据接收的处理方法:NAPI(New API),NAPI 是一种高效的网络处理技术.

  • 核心思想:中断(用来唤醒数据接收服务程序)+ 轮询(在接受服务程序中采用POLL的方法来轮询处理数据)

  • 目前 NAPI 已经在 Linux 的网络驱动中得到了大量的
    应用

  • 如何在驱动中使用 NAPI处理机制文章来源地址https://www.toymoban.com/news/detail-404142.html

/*初始化NAPI==1111111111111111111111111111111111111111111111
@	定义在 net/core/dev.c中
@	初始化一个 napi_struct实例
@	dev:每个NAPI 必须关联一个网络设备,此参数指定NAPI要关联的网络设备。
@	napi:要初始化的 NAPI实例
@	poll: NAPI所使用的轮询函数,非常重要,一般在此轮询函数中完成网络数据接收的工作。
@	weight:NAPI默认权重(weight),一般为NAPI_POLL_WEIGHT。 
@	返回值:无。
*/
void netif_napi_add(struct net_device   *dev,   struct napi_struct    *napi, int (*poll)(struct napi_struct *int        weight) 
 
 /*  删除NAPI ==2222222222222222222222222222222222222222222222
 @	napi:要删除的NAPI
 @	返回值:无。 
 */
 void netif_napi_del(struct napi_struct    *napi) 
 
 /*  使能NAPI ==333333333333333333333333333333333333333333333
 @	n:要使能的NAPI
 @	返回值:无。 
 */
inline void napi_enable(struct napi_struct    *n) 

/*关闭NAPI ===444444444444444444444444444444444444444444444444
@	n:要关闭的NAPI。 
@	返回值:无。 
*/
void napi_disable(struct napi_struct *n) 

 /* 检查NAPI是否可以进行调度 ==555555555555555555555555555555555
 @	n:要检查的NAPI
 @	返回值:如果可以调度就返回真,如果不可调度就返回假。 
 */
inline bool napi_schedule_prep(struct napi_struct *n) 

/*NAPI调度 ===6666666666666666666666666666666666666666666666666
@	n:要调度的NAPI。 
@	返回值:无。 
*/
void __napi_schedule(struct napi_struct *n) 

/*是否可以调度+调度 ===5555555555555555+666666666666666666666
@	n:要调度的NAPI。 
*/
 static inline void napi_schedule(struct napi_struct *n) 
 { 
     if (napi_schedule_prep(n)) 
         __napi_schedule(n); 
 } 

/*NAPI处理完成 ===77777777777777777777777777777777777777777777777
@	n:处理完成的NAPI。
@	返回值:无。 
*/
inline void napi_complete(struct napi_struct *n) 

到了这里,关于linux内核网络驱动框架(linux驱动开发篇)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux驱动开发——内核模块

    目录 内核模块的由来 第一个内核模块程序  内核模块工具  将多个源文件编译生成一个内核模块  内核模块参数 内核模块依赖 关于内核模块的进一步讨论  习题 最近一直在玩那些其它的技术,眼看快暑假了,我决定夯实一下我的驱动方面的技能,迎接我的实习,找了一本

    2024年02月04日
    浏览(56)
  • linux-2.6.22.6内核i2c驱动框架源码分析

    i2c是常见的通信协议,协议比较简单,只有数据和时钟两条线(SDA和SCL),i2c的通信分为主机和从机,主机一般占主导地位,从机可以有多个。 i2c通信的数据格式为(SDA上的数据):开始的7位里面指定了设备地址(因为有多个从机),第8位是读或写信号,表示此次传输是读还

    2024年02月11日
    浏览(41)
  • linux驱动开发:Linux 内核的一些函数

    1 、 MKDEV ( ma, mi ) 构造设备号,将主设备号和次设备号转换为设备号类型(dev_t)。 MKDEV 宏将主设备号( ma )左移 20 位,然后与次设备号( mi )相与,得到设备号。 dev_t 结构 主设备号 12 位

    2024年02月17日
    浏览(29)
  • 怎么降低Linux内核驱动开发的风险?

    降低Linux内核驱动开发的风险是一个重要的目标,因为内核驱动开发可能会对系统的稳定性和安全性产生重要影响。以下是一些降低风险的建议: 1. 深入了解Linux内核:在开始内核驱动开发之前,建议深入学习Linux内核的工作原理和架构,包括内核模块、设备模型、调度机制等

    2024年02月08日
    浏览(24)
  • Linux驱动开发(十五)---如何使用内核现有驱动(显示屏)

    《Linux驱动开发(一)—环境搭建与hello world》 《Linux驱动开发(二)—驱动与设备的分离设计》 《Linux驱动开发(三)—设备树》 《Linux驱动开发(四)—树莓派内核编译》 《Linux驱动开发(五)—树莓派设备树配合驱动开发》 《Linux驱动开发(六)—树莓派配合硬件进行字

    2024年02月15日
    浏览(28)
  • <Linux开发>驱动开发 -之-内核定时器与中断

    <Linux开发>驱动开发 -之-内核定时器与中断 交叉编译环境搭建: <Linux开发> linux开发工具-之-交叉编译环境搭建 uboot移植可参考以下: <Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分) <Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分) <Linux开

    2024年02月08日
    浏览(32)
  • linux驱动开发 - 08_内核定时器

    链接: C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂 1.1 内核时间管理简介 Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于驱动编写者来说最常用的定时器。 硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以

    2024年02月02日
    浏览(22)
  • 嵌入式Linux驱动开发 02:将驱动程序添加到内核中

    在上一篇文章 《嵌入式Linux驱动开发 01:基础开发与使用》 中我们已经实现了最基础的驱动功能。在那篇文章中我们的驱动代码是独立于内核代码存放的,并且我们的驱动编译后也是一个独立的模块。在实际使用中将驱动代码放在内核代码中,并将驱动编译到内核中也是比较

    2023年04月09日
    浏览(57)
  • [驱动开发]Linux内核定时器与中断的简单应用

    首先介绍一下定时器原理。 在linux系统中定时器有分为软定时和硬件定时器。 以海思某款芯片为例,定时器模块又称为Timer模块,主要实现定时、计数功能。 Timer 具有以下特点: 带可编程 8 位预分频器的 32bit/16bit 减法定时器/计数器。 Timer 的计数时钟为 3MHz 时钟。 支持 3 种

    2024年02月20日
    浏览(28)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包