[HAL库分析—GPIO]

文章目录

前言

开发板为:野火指南者,基于STM32F103VET6

分析的例程为:指南者\1-程序源码_教程文档\2-[野火]《STM32 HAL库开发实战指南》(HAL库源码)\12-GPIO输出---使用固件库点亮LED灯

1.GPIO

通过HAL库操作GPIOB的关键函数数据结构如下:

c 复制代码
// 1、使能GPIO时钟
__HAL_RCC_GPIOB_CLK_ENABLE()
 // 2、配置GPIO工作模式
void HAL_GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_Init)
 // 3、控制GPIOB输出状态
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)

接下来逐行分析上述的3行代码

1.1.使能GPIOB时钟

__HAL_RCC_GPIOB_CLK_ENABLE()STM32F1xx_HAL_Driver\Inc\stm32f1xx_hal_rcc.h文件中的511行,如下面代码所示

c 复制代码
// 🚀1、
#define __HAL_RCC_GPIOB_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg; \
                                        SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);\
                                        /* Delay after an RCC peripheral clock enabling */\
                                        tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);\
                                        UNUSED(tmpreg); \
                                      } while(0U)
c 复制代码
// 🚀2、展开👇
#define __HAL_RCC_GPIOB_CLK_ENABLE()  do { \
                                        __IO uint32_t tmpreg; \
                                        (RCC->APB2ENR) |= (RCC_APB2ENR_IOPBEN);\
                                        /* Delay after an RCC peripheral clock enabling */\
                                        tmpreg = (RCC->APB2ENR) & (RCC_APB2ENR_IOPBEN);\
                                        (void)tmpreg;\
                                      } while(0U)
                                      
// 🚀3、删掉不重要的代码得到👇
#define __HAL_RCC_GPIOB_CLK_ENABLE()  (RCC->APB2ENR) |= (RCC_APB2ENR_IOPBEN);
c 复制代码
// 🚀4、关于RCC以及RCC_APB2ENR_IOPBEN的定义👇
typedef struct
{
  __IO uint32_t CR;			// 偏移0字节
  __IO uint32_t CFGR;			// 偏移4字节
  __IO uint32_t CIR;			// 偏移8字节
  __IO uint32_t APB2RSTR;	// 偏移12字节
  __IO uint32_t APB1RSTR;	// 偏移16字节
  __IO uint32_t AHBENR;		// 偏移20字节
  __IO uint32_t APB2ENR;	// 偏移24字节, 即偏移0x18字节
  __IO uint32_t APB1ENR;
  __IO uint32_t BDCR;
  __IO uint32_t CSR;
} RCC_TypeDef;

#define RCC  ((RCC_TypeDef *)RCC_BASE)

RCC_BASE = (AHBPERIPH_BASE + 0x00001000UL) 
         = ((PERIPH_BASE + 0x00020000UL) + 0x00001000UL) 
         = ((0x40000000UL+ 0x00020000UL) + 0x00001000UL) 

RCC_APB2ENR_IOPBEN = (0x1UL << RCC_APB2ENR_IOPBEN_Pos) = (0x1UL << 3U) = 0000 0000 0000 1000 = 0x00000008UL
c 复制代码
// 🚀5、把第🚀4部分代码合并到第🚀3部分后得到👇
RCC->APB2ENR = *(unsigned int *)(((0x40000000UL+ 0x00020000UL) + 0x00001000UL) + 0x00000018UL)
RCC->APB2ENR |= 0x00000008UL

✅通过查询《STM32F10xxx参考手册》的2.3存储器映像章节得知
复位和时钟控制(RCC) 的基地址 = ((0x40000000UL+ 0x00020000UL) + 0x00001000UL)) = 0x40021000UL

✅通过查询《STM32F10xxx参考手册》的6.3.11 RCC寄存器地址映像章节得知
RCC_APB2ENR的地址 =(((0x40000000UL+ 0x00020000UL) + 0x00001000UL) + 0x00000018UL) = 0x40021018UL

✅通过查询通过查询《STM32F10xxx参考手册》6.3.7 APB2外设时钟使能寄存器(RCC_APB2ENR)章节得知
RCC_APB2ENRbit3是用于使能GPIOB的时钟

1.2.配置GPIO工作方式

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)STM32F1xx_HAL_Driver\Src\stm32f1xx_hal_gpio.c文件中的178行,函数定义如下

c 复制代码
/**
  * @brief  Initializes the GPIOx peripheral according to the specified parameters in the GPIO_Init.
  * @param  GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
  * @param  GPIO_Init: pointer to a GPIO_InitTypeDef structure that contains
  *         the configuration information for the specified GPIO peripheral.
  * @retval None
  */
void HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{
  uint32_t position = 0x00u;
  uint32_t ioposition;
  uint32_t iocurrent;
  uint32_t temp;
  uint32_t config = 0x00u;
  __IO uint32_t *configregister; /* Store the address of CRL or CRH register based on pin number */
  uint32_t registeroffset;       /* offset used during computation of CNF and MODE bits placement inside CRL or CRH register */

  /* Check the parameters */
  assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));
  assert_param(IS_GPIO_PIN(GPIO_Init->Pin));
  assert_param(IS_GPIO_MODE(GPIO_Init->Mode));

  /* Configure the port pins */
  while (((GPIO_Init->Pin) >> position) != 0x00u)
  {
    /* Get the IO position */
    ioposition = (0x01uL << position);

    /* Get the current IO position */
    iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;

    if (iocurrent == ioposition)
    {
      /* Check the Alternate function parameters */
      assert_param(IS_GPIO_AF_INSTANCE(GPIOx));

      /* Based on the required mode, filling config variable with MODEy[1:0] and CNFy[3:2] corresponding bits */
      switch (GPIO_Init->Mode)
      {
        /* If we are configuring the pin in OUTPUT push-pull mode */
        case GPIO_MODE_OUTPUT_PP:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP;
          break;

        /* If we are configuring the pin in OUTPUT open-drain mode */
        case GPIO_MODE_OUTPUT_OD:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_OD;
          break;

        /* If we are configuring the pin in ALTERNATE FUNCTION push-pull mode */
        case GPIO_MODE_AF_PP:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_PP;
          break;

        /* If we are configuring the pin in ALTERNATE FUNCTION open-drain mode */
        case GPIO_MODE_AF_OD:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_OD;
          break;

        /* If we are configuring the pin in INPUT (also applicable to EVENT and IT mode) */
        case GPIO_MODE_INPUT:
        case GPIO_MODE_IT_RISING:
        case GPIO_MODE_IT_FALLING:
        case GPIO_MODE_IT_RISING_FALLING:
        case GPIO_MODE_EVT_RISING:
        case GPIO_MODE_EVT_FALLING:
        case GPIO_MODE_EVT_RISING_FALLING:
          /* Check the GPIO pull parameter */
          assert_param(IS_GPIO_PULL(GPIO_Init->Pull));
          if (GPIO_Init->Pull == GPIO_NOPULL)
          {
            config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_FLOATING;
          }
          else if (GPIO_Init->Pull == GPIO_PULLUP)
          {
            config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD;

            /* Set the corresponding ODR bit */
            GPIOx->BSRR = ioposition;
          }
          else /* GPIO_PULLDOWN */
          {
            config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD;

            /* Reset the corresponding ODR bit */
            GPIOx->BRR = ioposition;
          }
          break;

        /* If we are configuring the pin in INPUT analog mode */
        case GPIO_MODE_ANALOG:
          config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_ANALOG;
          break;

        /* Parameters are checked with assert_param */
        default:
          break;
      }

      /* Check if the current bit belongs to first half or last half of the pin count number
       in order to address CRH or CRL register*/
      configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL     : &GPIOx->CRH;
      registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);

      /* Apply the new configuration of the pin to the register */
      MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset), (config << registeroffset));

      /*--------------------- EXTI Mode Configuration ------------------------*/
      /* Configure the External Interrupt or event for the current IO */
      if ((GPIO_Init->Mode & EXTI_MODE) == EXTI_MODE)
      {
        /* Enable AFIO Clock */
        __HAL_RCC_AFIO_CLK_ENABLE();
        temp = AFIO->EXTICR[position >> 2u];
        CLEAR_BIT(temp, (0x0Fu) << (4u * (position & 0x03u)));
        SET_BIT(temp, (GPIO_GET_INDEX(GPIOx)) << (4u * (position & 0x03u)));
        AFIO->EXTICR[position >> 2u] = temp;


        /* Configure the interrupt mask */
        if ((GPIO_Init->Mode & GPIO_MODE_IT) == GPIO_MODE_IT)
        {
          SET_BIT(EXTI->IMR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->IMR, iocurrent);
        }

        /* Configure the event mask */
        if ((GPIO_Init->Mode & GPIO_MODE_EVT) == GPIO_MODE_EVT)
        {
          SET_BIT(EXTI->EMR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->EMR, iocurrent);
        }

        /* Enable or disable the rising trigger */
        if ((GPIO_Init->Mode & RISING_EDGE) == RISING_EDGE)
        {
          SET_BIT(EXTI->RTSR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->RTSR, iocurrent);
        }

        /* Enable or disable the falling trigger */
        if ((GPIO_Init->Mode & FALLING_EDGE) == FALLING_EDGE)
        {
          SET_BIT(EXTI->FTSR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->FTSR, iocurrent);
        }
      }
    }

	position++;
  }
}

✅假设我们执行的代码如下

c 复制代码
#define GPIOB  (GPIO_TypeDef *) GPIOB_BASE

GPIO_InitTypeDef  GPIO_InitStruct;									   
GPIO_InitStruct.Pin = GPIO_PIN_5;	
GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;  
GPIO_InitStruct.Pull  = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);	

✅由于GPIO_InitStruct没有配置外部中断,那么我们可以把void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)外部中断的配置代码断言代码switch中非GPIO_MODE_OUTPUT_PP模式的代码删掉,便于分析代码,于是得到👇

c 复制代码
// 🚀1、简化后的HAL_GPIO_Init函数定义👇
void HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{
  uint32_t position = 0x00u;
  uint32_t ioposition;
  uint32_t iocurrent;
  uint32_t temp;
  uint32_t config = 0x00u;
  __IO uint32_t *configregister; 
  uint32_t registeroffset;    
  // 遍历并配置GPIO_Init->Pin的每一个引脚,从STM32F1xx_HAL_Driver\Inc\stm32f1xx_hal_gpio.h文件的第83行可以知道
  // GPIOB有16个引脚宏,每个引脚对应一个bit位,GPIO_Init->Pi = GPIO_PIN_5 = 0x0010 = 0000 0000 0001 0000对应了bit5
  while (((GPIO_Init->Pin) >> position) != 0x00u)
  {
    // ioposition在while循环中从0000 0000 000 0001 -> 1000 0000 000 010 -> ... -> 1000 0000 000 000
    ioposition = (0x01uL << position);
    // ioposition作为掩码和GPIO_Init->Pin进行按位与运算,找到需要配置的IO口
    iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
    // 当iocurrent == ioposition的时候,成功找到一个需要配置的IO口
    if (iocurrent == ioposition)
    {
      // 根据GPIO_Init->Mode对IO口进行工作方式的配置
      switch (GPIO_Init->Mode)
      {
        // 配置成开漏输出
        case GPIO_MODE_OUTPUT_PP:
          config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP;
          break;
      }
      /* Check if the current bit belongs to first half or last half of the pin count number
       in order to address CRH or CRL register*/
      configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL     : &GPIOx->CRH;
      registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);
      /* Apply the new configuration of the pin to the register */
      MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset), (config << registeroffset));
   }
   position++;
}
c 复制代码
// 🚀2、从🚀1中我们可以看出,配置寄存器的就这4行代码
config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP;
configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH;
registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);
MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset), (config << registeroffset));
c 复制代码
// 🚀3、我们继续
// 所以现在的分析思路要转换一下,我们要找到作者到底要操作哪个寄存器,然后查《STM32F10xxx参考手册》,所以先展开MODIFY_REG
#define READ_REG(REG)  ((REG))
#define MODIFY_REG(REG, CLEARMASK, SETMASK)  WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
// MODIFY_REG(REG, CLEARMASK, SETMASK)👉相当于👉REG = (REG & (~CLEARMASK)) | (SETMASK)

// 🚀4、所以作者要操作的寄存器是REG = (*configregister) = &GPIOx->CRL = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH
// GPIOx = GPIOB,GPIOB的定义如下
typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
#define PERIPH_BASE           0x40000000UL
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x00000C00UL)
#define GPIOB  ((GPIO_TypeDef *)GPIOB_BASE)
// 所以作者要操作的寄存器是
configregister = &GPIOx->CRL = (0x40000000UL + 0x00010000UL) + 0x00000C00UL = 0x40010C00UL
c 复制代码
// 🚀5、把MODIFY_REG宏展开后得到
// MODIFY_REG(REG, CLEARMASK, SETMASK)👉相当于👉REG = (REG & (~CLEARMASK)) | (SETMASK)

// REG = (*(&GPIOx->CRL)) = (*0x40010C00UL)

// ~CLEARMASK = ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset)
//            = ((0x3UL | (0x3UL << 2U)) << 20) 
//            = ~((0011  | (1100)) << 20) 
//            = 1111 1111 0000 1111 1111 1111 1111 1111b

// SETMASK = (config << registeroffset)
//         = (0x3UL << 20)
//         = 1111 1111 0011 1111 1111 1111 1111 1111b

// 即把GPIOB pin5 设置为 推挽输出模式,最大速度50MHZ
(*0x40010C00UL) = ((*0x40010C00UL) & (1111 1111 0000 1111 1111 1111 1111 1111b)) | 1111 1111 0011 1111 1111 1111 1111 1111b

✅通过查询《STM32F10xxx参考手册》的2.3存储器映像章节得知
GPIO端口B的基地址 = ((0x40000000UL + 0x00010000UL)+ 0x00000C00UL) = 0x40010C00UL

✅通过查询《STM32F10xxx参考手册》的8.5 GPIO和AFIO寄存器地址映象章节得知
GPIO端口B的基地址 = ((0x40000000UL + 0x00010000UL)+ 0x00000C00UL) = 0x40010C00UL0偏移时,对应寄存器GPIOx_CRL

✅通过查询《STM32F10xxx参考手册》的8.2.1 端口配置低寄存器(GPIOx_CRL)章节得知

寄存器GPIOx_CRL,负责配置GPIOx的第0-7个引脚,对应手册中y = 0-7,每个引脚占用4位的寄存器

从这里可以推断出为什么registeroffset需要左移2位 ,因为position代表着当前while循环中配置第几个引脚,position << 2相当于position * 4,正好复合手册中每个引脚需要占用GPIOx_CRL的4bit的描述,而由于一共有16个引脚,所以一个32bit的寄存器只能管理8个引脚,从这里可以推断出为什么((position - 8u) << 2u)

c 复制代码
registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);

✅在推导出结果后,我们知道void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)函数就是用来配置GPIOx_CRLGPIOx_CRH寄存器的。此时我们回头重新看👇三行代码,就很清晰了

c 复制代码
config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP; // config = 0x3UL + 0x00000000u
configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH;
registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);
  1. GPIO_Init->Speed:用来配置GPIOx_CRL或GPIOx_CRH的MODEy[1:0]
  2. GPIO_CR_CNF_GP_OUTPUT_PP:用来配置GPIOx_CRL或GPIOx_CRH的CNFy[1:0]
  3. configregister :用来确认当前配置的是GPIOx_CRL还是GPIOx_CRH
  4. registeroffset :用来确认当前配置的是GPIOx_CRL或GPIOx_CRH的哪个引脚

1.3.控制GPIOB输出状态

HAL_GPIO_WritePin()STM32F1xx_HAL_Driver\Src\stm32f1xx_hal_gpio.c文件中的465行,如下面代码所示

c 复制代码
// 🚀1
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  assert_param(IS_GPIO_PIN_ACTION(PinState));

  if (PinState != GPIO_PIN_RESET)
  {
    GPIOx->BSRR = GPIO_Pin;
  }
  else
  {
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;
  }
}
c 复制代码
// 🚀2、简化后得到
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
  if (PinState != GPIO_PIN_RESET)
    GPIOx->BSRR = GPIO_Pin;
  else
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;
}

✅假设我们执行的代码如下

c 复制代码
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET)

✅关键代码就2行

c 复制代码
// 🚀3、操作寄存器的关键代码
// 置位
GPIOx->BSRR = GPIO_Pin;
// 复位
GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;
c 复制代码
// 🚀4、继续展开代码
// GPIOx = GPIOB,GPIOB的定义如下
typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
#define PERIPH_BASE           0x40000000UL
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x00000C00UL)
#define GPIOB  ((GPIO_TypeDef *)GPIOB_BASE)

&GPIOx->BSRR = ((0x40000000UL + 0x00010000UL) + 0x00000C00UL) + 0x10;

✅同样的套路,先看《STM32F10xxx参考手册》的2.3章节找基地址,接着根据地址偏移量0x10找到具体的寄存器,从手册中可以发现,要置位或清零,直接往对应的引脚位写1即可

2.不用库函数,点亮LED灯

✅电路原理图如下,我们只要让GPIOB的PIN1,推挽输出模式,输出低电平,即可让LED_B蓝色亮起来

c 复制代码
int main(void)
{
	SystemClock_Config();
	// 1. RCC_APB2ENR
	*(unsigned int*)0x40021018 |= 0x08UL;
	// 2. GPIOx_CRL
	*(unsigned int*)0X40010C00 = (*(unsigned int*)0X40010C00) & (~0xF0UL);
	*(unsigned int*)0X40010C00 |= 0x30UL;
	// 3. GPIOx_BSSR
	*(unsigned int*)0X40010C10 |= (0x01UL << 16);
}

3.总结

GPIO的使用,分3步走:

  1. 使能GPIO的时钟,寄存器:RCC_APB2ENR
  2. 配置GPIO的工作模式,寄存器:GPIOx_CRLGPIOx_CRH
  3. 控制GPIO的输出状态,寄存器:GPIOx_BSSR
相关推荐
水饺编程3 小时前
第3章,[标签 Win32] :处理 WM_PRINT 消息
c语言·c++·windows·visual studio
虚假程序设计3 小时前
pythonnet 调用C接口
c语言·python
徐某人..4 小时前
网络编程学习--第一天
arm开发·单片机·学习·arm
yrx0203074 小时前
STM32F103通过L298N驱动两相4线步进电机【42步进电机】
stm32·单片机·嵌入式硬件·步进电机
是大强4 小时前
3d打印材料asa和abs区别
嵌入式硬件
周周记笔记5 小时前
LC项目实战一:原理图DRC(二)
嵌入式硬件·pcb
安当加密5 小时前
基于 SLA 的操作系统双因素安全登录:USB Key 与 OTP 动态口令实践
单片机·嵌入式硬件·安全
硅农深芯5 小时前
六大核心芯片:MCU/SOC/DSP/FPGA/NPU/GPU 的区别与应用解析
单片机·嵌入式硬件·fpga开发
就是蠢啊5 小时前
51单片机——DS18B02(三)
单片机·嵌入式硬件·51单片机