C++ 特性简化STM32 风格固件库的GPIO 操作,使用HK32F030M

news2024/11/24 14:11:55

所谓的STM32 风格就是指下面这种:

// 开启时钟
RCC_AHBPeriphClockCmd( LED1_GPIO_CLK | LED2_GPIO_CLK, ENABLE);

//定义初始化结构体
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;

//初始化引脚
GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;
GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);	

GPIO_InitStructure.GPIO_Pin = LED2_GPIO_PIN;
GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStructure);

//操作引脚
GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
GPIO_SetBits(LED2_GPIO_PORT, LED2_GPIO_PIN);

已经习惯了的人可能会觉得这样完全没毛病,但是我更习惯8 位单片机和Arduino 那种风格,操作GPIO 这种常用的东西就应该信手拈来才对,而不是还要酝酿一阵子,或者到处复制粘贴代码过来。虽说32 位单片机本身更复杂了,操作上更复杂也情有可原,但是有些地方借助c++ 的“新”特性还是可以做成更舒服的样子的。

引脚定义

首先要处理的就是引脚定义的问题。上面的代码中,想完整的使用一个引脚,要涉及好几个东西:

  • 这个引脚的Port 指针 LED1_GPIO_PORT
  • 引脚的Pin LED1_GPIO_PIN
  • 时钟 LED1_GPIO_CLK
  • GPIO_PinSource0 一类的宏,有几个固件库函数也要用到,

于是常见的写法就是像上面的代码那样,一个引脚要定义好几个宏,东一榔头西一棒槌的,连最简单的给引脚置高电平都得同时引用 LED1_GPIO_PORTLED1_GPIO_PIN 这两个宏,相比之下,曾经我们只需要写:

// 51 单片机
LED1 = 1;

// Arduino (Wiring)
digitalWrite(LED1, 1);

// AVR
LED1_GPIO_PORT |= _BV(LED1_GPIO_PIN);

心智负担明显小多了,时间和精力也是很宝贵的,除了AVR,倒是和STM32 的风格差不多[doge]。希望实现的效果类似下面这样:

// 分配PA0 引脚为LED1
LED1 = PA0;

// 点亮LED1,置高电平
setpin(LED1);

非常的biu 特佛,一眼就能看出LED1 是干嘛的,简洁明快,当然这只是用不了的伪代码,反正意思就是这个意思。思路是用一个结构体把这四项东西都包到一起,之后操作引脚只要把结构体拿过来就全都有了,如下:

// PortType 不能定义为GPIOA 的类型,因为GPIOA 是从GPIOA_BASE 强制转换的一个指针,
// 这种转换和reinterpret_cast 是一样的,不能用在constexpr 的初始化过程中,
// 后面要用GPIOA 指针的地方再手动转换一下。
using PortType = decltype(GPIOA_BASE);
using PortClkEnableType = decltype(RCC_AHBPeriph_GPIOA);
using PinType = decltype(GPIO_Pin_0);
using PinSourceType = decltype(GPIO_PinSource0);

struct PinToken {
    PortType port;
    PortClkEnableType port_clk_en;
    PinType pin;
    PinSourceType pin_source;
};

注意,从这里开始,下面的代码就全是C++ 了,源代码后缀名要写成.cpp 或.cxx,用Keil MDK 的话要指定c++ 标准在c++ 11 以上,我现在用的c++ 14。

代码上半部分的using 是用来获取固件库中这些宏的数据类型,定义结构体的时候要用。具体来说,decltype(GPIO_Pin_0) 就是获取固件库头文件中GPIO_Pin_0 这个常数的数据类型,HK32F030M 的库文件里是uint16_t,用这种写法而不是写死是为了兼容性,万一其他单片机的库里不是uint16_t 也不用改代码。就问C 能做到吗~ [doge]

下面的结构体就没什么好说的,只是不用像C 一样再加上typedef 了,关键是用法。现在要定义一堆结构体常量用来包装每个引脚的四项信息,用宏是做不到的,如果C 来实现,一堆结构体肯定会占用一堆程序空间,好在C++ 11 以后有了constexpr,可以定义这种自定义类型的字面常量,而不付出空间上的代价:

// Port A
// 定义并初始化结构体常量,把每个引脚的相关信息存进去
constexpr PinToken PA0 = {GPIOA_BASE, RCC_AHBPeriph_GPIOA, GPIO_Pin_0, GPIO_PinSource0};
constexpr PinToken PA1 = {GPIOA_BASE, RCC_AHBPeriph_GPIOA, GPIO_Pin_1, GPIO_PinSource1};

// Port D
constexpr PinToken PD0 = {GPIOD_BASE, RCC_AHBPeriph_GPIOD, GPIO_Pin_0, GPIO_PinSource0};

上面这样一个一个写还是太麻烦了,可以写一个辅助宏:

// 用来生成引脚定义的宏,展开形式类似如下:
//   constexpr PinToken PA0 = {GPIOA_BASE, RCC_AHBPeriph_GPIOA, GPIO_Pin_0, GPIO_PinSource0}
#define _GPIO_DEF_PIN_TOKEN(PORT, PIN) \
    constexpr PinToken P##PORT##PIN = {GPIO##PORT##_BASE, RCC_AHBPeriph_GPIO##PORT, GPIO_Pin_##PIN, GPIO_PinSource##PIN}

// 用宏生成PD6 的定义
_GPIO_DEF_PIN_TOKEN(D, 6);

// 就等效于下面这样:
constexpr PinToken PD6 = {GPIOD_BASE, RCC_AHBPeriph_GPIOD, GPIO_Pin_6, GPIO_PinSource6};

想了解这个宏的原理的话,可以去搜## 两个井号的作用,或者看看我写的宏魔法简单介绍:C51 实现Arduino 式的IO 引脚编号映射和统一的IO 操作 - C语言宏魔法的简单实践。

按上面的写法,可以继续定义全部引脚。constexpr 的效果这里就简单提一句,这种方法定义的“值” 被称为字面量或者编译器常量,字面量就是代码里手写的一个3 这类东西,而编译器常量的意思是说,编译器可以确定这些值一经定义就不会改变,就像用宏定义的常量一样,编译完就没有了,不会专门留一块空间存储它,随用随扔,和汇编里的立即数也差不多。

不管定义多少个引脚,这些常量本身不占用空间,但是就像代码中手写的一个字面量一样,比如,还是3,虽然没专门存储它,但是3 这个信息肯定会体现在代码中,如果用3 作为参数调用一个函数,那么传递参数的时候,根据3 的类型,可能会占用一个字的栈空间,不过返回之后栈空间就回收了,所以也不用太紧张。当然,还有往栈里压参数的指令,也要占用代码空间。

不能直接使用GPIOA 指针

另外,上面的注释中提到一个小注意事项,“PortType 不能定义为GPIOA 的类型,因为GPIOA 是从GPIOA_BASE 强制转换的一个指针”。GPIOA 是固件库中定义的指向外设寄存器的指针,固件库函数中都要使用它,也就是开头代码中的LED1_GPIO_PORT,但是结构体中却不能直接存放这个指针,原因是固件库中GPIOA 的定义:

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)

GPIOA_BASE 是外设寄存器的基地址,类型是uint32_t,也就是一个整数,而GPIOA 要当指针来用,所以就强制类型转换了一下,转换成GPIO_TypeDef 类型的指针,这种用法是ARM 官方的CMSIS 标准里规定的,但是把整数常量转换成指针后就不能用在constexpr 常量中了,这又是C++ 标准中的要求,详细的可以去搜“reinterpret_cast 为什么不能用在constexpr”。结果只好在结构体里存GPIOA_BASE,后面操作函数时要用GPIOA 指针,就再转换一下。

引脚操作函数

要愉快的使用上面定义好的引脚结构体,先从简单的引脚操作函数开始,也就是读、写、翻转这三种操作,首先改写固件库里的设置高低电平的函数:

// 固件库函数
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  GPIOx->BSRR = GPIO_Pin;
}

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  GPIOx->BRR = GPIO_Pin;
}


/* 改写 */
// 置位,也就是写1
inline static void setpin(PinToken pin) {
    _GPIO_PORT_TO_POINTER(pin.port)->BSRR = pin.pin;
}

// 清零,也就是写0
inline static void clrpin(PinToken pin) {
    _GPIO_PORT_TO_POINTER(pin.port)->BRR = pin.pin;
}

改写前后一对比其实差别很小,GPIO_SetBits 对应setpin,给引脚置1,即高电平,GPIO_ResetBits 对应clrpin,给引脚置0,即低电平,clr 是很常见的简写,意思是clear,清除,汇编指令中常见CLB 这种写法,其中的CL 也是clear。改写后的版本去掉了参数检查,因为按设计,形参pin 接受的实参只有上面定义过的那些结构体常量,使用者只管用就行了,如果非要自己定义一个不合法的结构体塞进去,那程序员自然是没有客户那种待遇的,硬要酒吧里吃炒面的后果只能自己承担。

更重要的是,改写后的函数加了inline 修饰,只要不把编译器优化关掉,这两个函数的调用会被编译器优化掉,函数会像宏一样原地展开,又因为传入的参数pin 是结构体常量,编译器能直接看到pin.pinpin.port 的值是什么,所以就把值直接塞进去了。于是编译之后,其实和直接使用寄存器的代码是一样的,没有额外的开销。

里面那个宏_GPIO_PORT_TO_POINTER 用来把结构体中的GPIOA_BASE 转换成GPIOA 指针,内容很简单:

#define _GPIO_PORT_TO_POINTER(port) reinterpret_cast<decltype(GPIOA)>(port)   // 做一次类型转换,把整数port 转换成GPIOA 对应的指针类型。

用相同的思路,接着实现剩下的操作函数:

// 读取一个引脚的值,结果为0 表示低电平,非0 为高电平
// 注意,非0 不一定就等于1
inline static auto test_pin(PinToken pin) {
    return _GPIO_PORT_TO_POINTER(pin.port)->IDR & pin.pin;
}

// 引脚电平翻转
inline static void toggle_pin(PinToken pin) {
    _GPIO_PORT_TO_POINTER(pin.port)->ODR ^= pin.pin;
}

// 有些人可能喜欢这种方法设置电平
// level 为0 则设置低电平,否则高电平。如果level 是手写的字面量,那么函数里的if else 也可能被编译器优化掉
inline static void write_pin(PinToken pin, uint8_t level) {
    if (level == 0) {
        clrpin(pin);
    }
    else {
        setpin(pin);
    }
}

这样就差不多了,常用的操作就这么几个,用法如下:

// 先定义两个LED 的引脚
constexpr auto LED1 = PA0;
constexpr auto LED2 = PD7;

//喜欢的话也可以写成宏
#define LED3  PA2

// 置高电平
setpin(LED1);
setpin(LED2);

// 置低电平
clrpin(LED1);
clrpin(LED2);

美中不足的地方是,库函数GPIO_SetBitsGPIO_ResetBits 可以一次设置多个引脚,只要全在同一个Port,比如:

#define LED1_PORT  GPIOA
#define LED1_PIN GPIO_Pin_0
#define LED2_PORT  GPIOA
#define LED2_PIN GPIO_Pin_2

//因为LED1 和LED2 都在GPIOA,所以可以写在一起,同时置1
GPIO_SetBits(LED1_PORT, LED1_PIN | LED2_PIN);

虽然没什么用,万一以后改了LED2 的引脚,不在GPIOA 了,这么写还很容易导致BUG。不过想要的话,改写后的版本也能实现相同的功能,并且继承相同的BUG [doge],有点复杂,最后再说。

引脚初始化函数

也就是定义一个GPIO_InitTypeDef,然后一个个赋值,再一个一个初始化的部分。我不打算把固件库的代码全部改写掉,因为麻烦,而且换到其他单片机之后万一固件库底层实现不一样了就更麻烦了,不如就套一层皮,还是基于这个结构体赋值,只是优化一下写法,反正引脚初始化的部分整个程序中一般只要写一次,多包一层,付出的代价不会很大。需要实现的效果是,我想尽量不碰这个初始化结构体,怎么方便怎么来,比如下面这样:

// 初始化引脚的函数
// 引脚速度、上拉电阻、施密特这三项参数提供了默认值,分别是高速(10MHz)、带上拉、启用施密特
void init_pin(PinToken pin, pin_mode mode, pull_mode pull = pull_mode::up, speed sp = speed::high, schmit sh = schmit::enable) {
    GPIO_InitTypeDef init_struct = {.GPIO_Pin = pin.pin,
                                    .GPIO_Mode = static_cast<GPIOMode_TypeDef>(_GPIO_ENUM_TO_UNDERLYING(mode) & 0x00ff),
                                    .GPIO_Speed = _GPIO_ENUM_TO_ENUM(sp, GPIOSpeed_TypeDef),
                                    .GPIO_OType = static_cast<GPIOOType_TypeDef>(_GPIO_ENUM_TO_UNDERLYING(mode) >> 8),
                                    .GPIO_PuPd = _GPIO_ENUM_TO_ENUM(pull, GPIOPuPd_TypeDef),
                                    .GPIO_Schmit = _GPIO_ENUM_TO_ENUM(sh, GPIOSchmit_TypeDef)};
    GPIO_Init(_GPIO_PORT_TO_POINTER(pin.port), &init_struct);
}

// 使用方法
init_pin(LED1, pin_mode::out_pp)  // 初始化LED1 为推挽(pp)输出(out)模式,其他参数按默认值设置
init_pin(LED2, pin_mode::out_pp)  // LED2 同理

可见实现方式还是很笨的,主要就是靠函数的默认参数减负,函数内部就是根据参数创建了一个初始化结构体,拿去调用固件库的初始化函数GPIO_Init,之后这个结构体就丢弃掉了。其实效率上还行,因为这么一来,本来手写的创建结构体和赋值的步骤全部放在这个函数里了,可以复用,所以代码空间不会有太多浪费,只是每次初始化都要创建一次结构体,稍微费点时间。函数参数除了引脚结构体,就是pin_mode 等一堆枚举,这就是把固件库里的枚举值也重新封装了一遍,因为固件库里的枚举是C 的枚举,有一些用起来不爽的特性。

枚举

简单说一下C 语言枚举的问题,比如下面这个:

typedef enum
{
  GPIO_Mode_IN   = 0x00, /*!< GPIO Input Mode              */
  GPIO_Mode_OUT  = 0x01, /*!< GPIO Output Mode             */
  GPIO_Mode_AF   = 0x02, /*!< GPIO Alternate function Mode */
  GPIO_Mode_AN   = 0x03  /*!< GPIO Analog In/Out Mode      */
}GPIOMode_TypeDef;

这个枚举的大括号里面包了四个值,即GPIO_Mode_IN 这几个,但是和结构体成员不一样,枚举里的值是可以直接在外面引用的,也就是说:

// 不用先礼貌的通知一下枚举类型
func(GPIOMode_TypeDef.GPIO_Mode_IN)

// 而是直接把值拿出来用
func(GPIO_Mode_IN);

习惯了的人大概就习惯了,但这是存在问题的,就是污染全局作用域,和全局变量一样,一定义出来就到处都是,所以才不得不给每个枚举值都加上前缀,以免名字冲突。另一方面,这样也不方便使用IDE 的智能提示功能,比如结构体变量,打一个点号后面就会出来菜单提示你里面有哪些成员,而枚举值就只能看运气,所有相同前缀的东西都会蹦出来。

所以就拿C++ 的结构体重写了一下,只说pin_mode,这个结构体把固件库中的Mode 和OType 组合起来了,就是本来要分别设置成输出、推挽,改写之后pin_mode 枚举里面专门有一个模式out_pp 就是推挽输出类型,会同时设置Mode 和OType。

// 高8 字节表示输出类型,低8 字节为模式
enum class pin_mode {
    in = GPIO_Mode_IN,
    out_pp = GPIO_Mode_OUT | (GPIO_OType_PP << 8),
    out_od = GPIO_Mode_OUT | (GPIO_OType_OD << 8),
    af_pp = GPIO_Mode_AF | (GPIO_OType_PP << 8),  // AF 模式不需要手动设置,对应的外设函数会自动配置
    af_od = GPIO_Mode_AF | (GPIO_OType_OD << 8),
    an = GPIO_Mode_AN,
};

可见,就是用位运算把Mode 和OType 组合到一起了,然后在函数里再分解开,目的就是方便,少写一个参数。剩下的几个枚举基本是复制粘贴了固件库:

// 上拉/下拉电阻
enum class pull_mode {
    no = GPIO_PuPd_NOPULL,
    up = GPIO_PuPd_UP,
    down = GPIO_PuPd_DOWN
};

// 频率
enum class speed {
    low = GPIO_Speed_2MHz,
    high = GPIO_Speed_10MHz, 
    //high = GPIO_Speed_50MHz  HK32F030M 不支持50Mhz
};

// HK32F030M 可以关闭引脚的施密特触发器功能,不知道是不是Cortex-M0 单片机都有,关掉大概可以省电,默认打开
enum class schmit {
    disable = GPIO_Schmit_Disable,
    enable = GPIO_Schmit_Enable,
};

后面三个枚举对应的参数都有默认值,一般不用修改,所以初始化引脚大部分时候只用设置一个pin_mode 就行。

复用初始化结构体

如果还是想像固件库一样一个初始化结构体重复使用,可以给上面的init_pin 增加两个重载:

/**
 * @brief 按照输入参数初始化空的GPIO_TypeDef 结构体,然后初始化GPIO,传入的GPIO_TypeDef 可以被void init_pin(PinToken pin, GPIO_TypeDef &init_struct)复用。
 *
 * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
 * @param init_struct 初始化结构体的所有数据将被覆盖
 * @param mode 配置输入、输出、复用、模拟输入,以及推挽或开漏输出
 * @param pull 配置上拉、下拉或浮空
 * @param sp 配置速度
 * @param sh 配置输入施密特触发器
 */
void init_pin(PinToken pin, GPIO_InitTypeDef& init_struct, pin_mode mode, pull_mode pull=pull_mode::up, speed sp=speed::high, schmit sh=schmit::enable) {
    init_struct.GPIO_Pin = pin.pin;
    init_struct.GPIO_Mode = static_cast<GPIOMode_TypeDef>(_GPIO_ENUM_TO_UNDERLYING(mode) & 0x00ff);
    init_struct.GPIO_Speed = _GPIO_ENUM_TO_ENUM(sp, GPIOSpeed_TypeDef);
    init_struct.GPIO_OType = static_cast<GPIOOType_TypeDef>(_GPIO_ENUM_TO_UNDERLYING(mode) >> 8);
    init_struct.GPIO_PuPd = _GPIO_ENUM_TO_ENUM(pull, GPIOPuPd_TypeDef);
    init_struct.GPIO_Schmit = _GPIO_ENUM_TO_ENUM(sh, GPIOSchmit_TypeDef);
    GPIO_Init(_GPIO_PORT_TO_POINTER(pin.port), &init_struct);
}


/**
 * @brief 复用已经配置好的GPIO_TypeDef 结构体继续配置引脚
 *
 * 使用方法是先用带配置参数的init_pin 重载把初始化结构体配置好,然后复用该结构体,调用这个函数配置其他参数相同的引脚
 *
 * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
 * @param init_struct 只修改初始化结构体的GPIO_Pin,其他保持原样
 */
void init_pin(PinToken pin, GPIO_InitTypeDef& init_struct) {
    init_struct.GPIO_Pin = pin.pin;
    GPIO_Init(_GPIO_PORT_TO_POINTER(pin.port), &init_struct);
}

这两个函数的第二个参数是初始化结构体的引用,调用函数前先创建一个空白的初始化结构体,然后调用第一个函数,函数里面会根据参数初始化传入的结构体,并配置引脚。第二个函数没有用来配置引脚模式的参数,是用来批量配置多个相同参数的引脚的,直接拿传入的结构体配置传入的引脚,所以必须先调用第一个函数,然后才能使用第二个,如下:

GPIO_InitTypeDef init_struct;
//初始化LED1 为推挽输出,其他参数按默认值
init_pin(LED1, init_struct, pin_mode::out_pp);
//用相同的参数初始化LED2
init_pin(LED2, init_struct);

链式调用

这样可能还嫌不够爽,比如要手动创建一个初始化结构体变量,调用函数的时候每次都要手动传入这个初始化结构体,那么还可以采用所谓的链式调用设计,实现方法是先定义一个类,如下:

class ChainInit {
   private:
    GPIO_InitTypeDef init_struct;
	
   public:
    /**
     * @brief 修改初始化结构体并配置引脚,返回值可以链式调用继续初始化下一个引脚
     *
     * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
     * @param init_struct 初始化结构体的所有数据将被覆盖
     * @param mode 配置输入、输出、复用、模拟输入,以及推挽或开漏输出
     * @param pull 配置上拉、下拉或浮空,默认为上拉
     * @param sp 配置速度,默认为高速/10MHz
     * @param sh 配置输入施密特触发器,默认使能
     * @return ChainInit&
     */
    inline ChainInit& init(PinToken pin, pin_mode mode, pull_mode pull = pull_mode::up, speed sp = speed::high, schmit sh = schmit::enable) {
        init_pin(pin, this->init_struct, mode, pull, sp, sh);
        return *this;
    }


    /**
     * @brief 复用已经配置好的GPIO_TypeDef 结构体继续配置引脚
     *
     * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
     * @return ChainInit&
     */
    inline ChainInit& init(PinToken pin) {
        init_pin(pin, this->init_struct);
        return *this;
    }
};

初始化结构体这次变成了private 成员变量,定义对象时就顺便有了初始化结构体;两个成员函数的用法和前面的init_pin 相似,只是现在它们会返回对象的引用,因此可以链式调用,用法如下:

//设置两个LED 为推挽输出,两个KEY 为输入
ChainInit()
  .init(LED1, pin_mode::out_pp)
  .init(LED2)
  .init(KEY1, pin_mode::in)
  .init(KEY2);

ChainInit() 没有绑定到一个变量,所以是创建了一个临时对象,用这个临时对象内部的初始化结构体依次初始化四个引脚,然后临时对象就被废弃。对象里只有一个初始化结构体,从存储空间的角度看,这个对象和初始化结构体是完全等同的;两个成员函数的内容都很简单,会被内联,所以调用成员函数和直接调用init_pin 没有区别,不会增加开销。

启动时钟函数

最后再来考虑操作顺序上排第一的这个函数。要简化操作,所以启动时钟要和其他GPIO 相关的函数放在一起,而不是像固件库那样放到RCC 那边,从而让整个GPIO 的操作更有整体感,像一个分离的模块。另外提一句,HK32F030M 的GPIO 模块是挂在AHB-Lite 总线上的,而不是像F103 单片机那样挂在APB 上,如下图:

在这里插入图片描述

因此,参考固件库的实现,启动时钟的函数可以写成下面这样:

inline static void enable_clk(PinToken pin) {
    // RCC_AHBPeriphClockCmd(pin.port_clk_en, ENABLE);
    RCC->AHBENR |= pin.port_clk_en;
}

// 用法
enable_clk(LED1);
enable_clk(LED2);

直接用寄存器操作,函数也带有inline 修饰,所以函数调用能被优化。但是用固件库的函数时可以好几个引脚一起当参数传进去,上面这种一个一个调用的写法太低效了。那么,首先就要求这个函数可以接受数量不确定的多个参数,其次,尽量不能损失效率。数组传参和VA_ARGS 这两种方案不能采用,前者又丑又低效,后者会让函数内容变复杂,无法内联优化,损失效率。

C++ 函数可变参数方案

还有两种方案,一种是给函数增加一大堆不同参数个数的重载,到时候要调用,不管有几个参数,都提前准备了对应参数数量的函数重载,比如这样:

// 一个参数
inline static void enable_clk(PinToken pin) {
    RCC->AHBENR |= pin.port_clk_en;
}

// 两个参数
inline static void enable_clk(PinToken pin1, PinToken pin2) {
    RCC->AHBENR |= (pin1.port_clk_en | pin1.port_clk_en);
}

// ...

//很多参数
inline static void enable_clk(PinToken pin1, PinToken pin2, PinToken pin3, /* ... */) {
    RCC->AHBENR |= (pin1.port_clk_en | pin2.port_clk_en | /* ... */);
}

//用法
enable_clk(LED1, LED2, KEY1, KEY2);   // 自动匹配四个参数的函数重载

看着挺傻的,但是这样确实能用,函数重载也可以用脚本自动生成,有些地方其实真的在用这种设计,而且这个简单函数会被内联优化掉,所以定义的重载再多也没事,不会多占用存储空间。第二种方案相当于让编译器自动在调用的时候生成对应参数数量的函数重载,也就是C++ 的模板元编程技术,从C++ 11 开始引入了变参模板,就可以用来“优雅”的实现可变参数,如下:

constexpr PortClkEnableType _calc_port_clk_sum(PinToken pin) {
    return pin.port_clk_en;
}

template <typename... Ts>
constexpr PortClkEnableType _calc_port_clk_sum(PinToken pin, Ts... args) {
    return pin.port_clk_en | _calc_port_clk_sum(args...);
}

template <typename... Ts>
inline static void enable_clk(PinToken pin, Ts... args) {
    RCC->AHBENR |= _calc_port_clk_sum(pin, args...);
}

template <typename... Ts>
inline static void disable_clk(PinToken pin, Ts... args) {
    RCC->AHBENR &= ~_calc_port_clk_sum(pin, args...);
}

//用法
enable_clk(LED1, LED2, KEY1, KEY2);   // 使用上倒是没什么要特别注意的,随便几个参数都行

前面两个函数用来递归的计算出(pin1.port_clk_en | pin2.port_clk_en | /* ... */) 这部分的值,然后enable_clk 再拿计算结果给寄存器赋值,启动GPIO 的时钟,disable_clk 是用来关闭时钟的函数,一般应该不太能用到。整个过程全部在编译期完成,不会占用运行时的资源。如果想让上面的setpinclrpin 函数可以一次设置多个引脚,也可以用相同的模板技巧。具体的细节原理我就不说了,网上一搜资料很多。

总结

最后就再加一个命名空间,把上面的定义的东西都放进命名空间里,完整的代码见[附录 - 1](#附录 - 1)。

虽然说了是用C++,但其实上面这些并没有用到什么面向对象的东西,全都是静态链接的,虚函数、多态、动态内存之类的让人“闻之色变”的东西都没涉及,这种风格就是所谓的Better C,Arduino 的库代码基本上也是这种风格,把C++ 当成更好使的C 来用,而不是把C++ 当作更混沌的Java,言必称对象,随时随地new。

另一方面,其实也能看到,原本的固件库的设计也不是一无是处,高情商叫灵活,低情商叫松散,在那种设计下,如果以后硬件上多加了什么功能,库代码要改动的地方很少,但是官方写库的人轻松了,锅就推给了用库的人。相对而言,C++ 的正经库的设计就有点走向另一个极端了,对写库的人素质要求很高,因为C++ 的哲学就是我全都要.jpg,既想要灵活,兼容性好,又想让用库的人写起来比较舒心,还想尽量不损失效率。于是往往是大神们先提出一些风骚但拧巴的设计,用来实现既要又要还要的理想,然后C++ 语言再把其中一些概念正式化,纳入语言本身,让写库的大神们体验更好。总之,我辈凡人还是要量力而为[doge]。

附录 - 1

#include <type_traits>

#include "hk32f030m.h"

// 用来生成引脚定义的宏,展开形式类似如下:
//   constexpr PinToken PA0 = {GPIOA_BASE, RCC_AHBPeriph_GPIOA, GPIO_Pin_0, GPIO_PinSource0}
#define _GPIO_DEF_PIN_TOKEN(PORT, PIN) \
    constexpr PinToken P##PORT##PIN = {GPIO##PORT##_BASE, RCC_AHBPeriph_GPIO##PORT, GPIO_Pin_##PIN, GPIO_PinSource##PIN}

#define _GPIO_PORT_TO_POINTER(port) reinterpret_cast<decltype(GPIOA)>(port)

#define _GPIO_ENUM_TO_UNDERLYING(e) static_cast<std::underlying_type_t<decltype(e)>>(e)

#define _GPIO_ENUM_TO_ENUM(e1, e2) static_cast<e2>(static_cast<std::underlying_type_t<decltype(e1)>>(e1))


/**
 * @brief 定义了HK32F030M 和0301M 所有GPIO 引脚,特定封装下可能不是全部可用
 *
 * 在F030M 下:
 * PA0 是NRST 引脚的复用功能,F030M 上可能无法启用;PD7 就是VCAP 引脚,可以用作GPIO,但不一定能使用复用功能。
 *
 * 在F0301M 下:
 * 包括PA0 和PD7 在内,共18 个IO 可用。复用NRST 引脚后,编程时只能使用上电复位。
 */
namespace gpio {

    // PortType 不能定义为GPIOA 的类型,因为GPIOA 是从GPIOA_BASE 强制转换的一个指针,
    // 这种转换和reinterpret_cast 是一样的,不能用在constexpr 的初始化过程中,
    // 后面要用GPIOA 指针的地方再手动转换一下。
    using PortType = decltype(GPIOA_BASE);
    using PortClkEnableType = decltype(RCC_AHBPeriph_GPIOA);
    using PinType = decltype(GPIO_Pin_0);
    using PinSourceType = decltype(GPIO_PinSource0);


    struct PinToken {
        PortType port;
        PortClkEnableType port_clk_en;
        PinType pin;
        PinSourceType pin_source;
    };


    // PAx
    _GPIO_DEF_PIN_TOKEN(A, 0);  // NRST 复用功能
    _GPIO_DEF_PIN_TOKEN(A, 1);
    _GPIO_DEF_PIN_TOKEN(A, 2);
    _GPIO_DEF_PIN_TOKEN(A, 3);

    // PBx
    _GPIO_DEF_PIN_TOKEN(B, 4);
    _GPIO_DEF_PIN_TOKEN(B, 5);

    // PCx
    _GPIO_DEF_PIN_TOKEN(C, 3);
    _GPIO_DEF_PIN_TOKEN(C, 4);
    _GPIO_DEF_PIN_TOKEN(C, 5);
    _GPIO_DEF_PIN_TOKEN(C, 6);
    _GPIO_DEF_PIN_TOKEN(C, 7);

    // PDx
    _GPIO_DEF_PIN_TOKEN(D, 1);
    _GPIO_DEF_PIN_TOKEN(D, 2);
    _GPIO_DEF_PIN_TOKEN(D, 3);
    _GPIO_DEF_PIN_TOKEN(D, 4);
    _GPIO_DEF_PIN_TOKEN(D, 5);
    _GPIO_DEF_PIN_TOKEN(D, 6);
    _GPIO_DEF_PIN_TOKEN(D, 7);  // VCAP 引脚


    constexpr PortClkEnableType _calc_port_clk_sum(PinToken pin) {
        return pin.port_clk_en;
    }

    template <typename... Ts>
    constexpr PortClkEnableType _calc_port_clk_sum(PinToken pin, Ts... args) {
        return pin.port_clk_en | _calc_port_clk_sum(args...);
    }

    template <typename... Ts>
    //__attribute__((always_inline)) inline static void enable_clk(const PinToken pin, const Ts... args) {
    inline static void enable_clk(PinToken pin, Ts... args) {
        // RCC_AHBPeriphClockCmd(_calc_port_clk_sum(pin, args...), ENABLE);
        RCC->AHBENR |= _calc_port_clk_sum(pin, args...);
    }

    template <typename... Ts>
    inline static void disable_clk(PinToken pin, Ts... args) {
        // RCC_AHBPeriphClockCmd(_calc_port_clk_sum(pin, args...), DISABLE);
        RCC->AHBENR &= ~_calc_port_clk_sum(pin, args...);
    }


    // 高8 字节表示输出类型,低8 字节为模式
    enum class pin_mode {
        in = GPIO_Mode_IN,
        out_pp = GPIO_Mode_OUT | (GPIO_OType_PP << 8),
        out_od = GPIO_Mode_OUT | (GPIO_OType_OD << 8),
        af_pp = GPIO_Mode_AF | (GPIO_OType_PP << 8),  // AF 模式不需要手动设置,对应的外设函数会自动配置
        af_od = GPIO_Mode_AF | (GPIO_OType_OD << 8),
        an = GPIO_Mode_AN,
    };

    enum class pull_mode {
        no = GPIO_PuPd_NOPULL,
        up = GPIO_PuPd_UP,
        down = GPIO_PuPd_DOWN
    };

    enum class speed {
        low = GPIO_Speed_2MHz,
        high = GPIO_Speed_10MHz,
        // high = GPIO_Speed_50MHz  不支持50Mhz
    };

    enum class schmit {
        disable = GPIO_Schmit_Disable,
        enable = GPIO_Schmit_Enable,
    };

    constexpr GPIO_InitTypeDef make_empty_init() {
        return GPIO_InitTypeDef{};
    }


    /**
     * @brief 按照输入参数初始化空的GPIO_TypeDef 结构体,然后初始化GPIO,传入的GPIO_TypeDef 可以被void init_pin(PinToken pin, GPIO_TypeDef &init_struct)复用。
     *
     * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
     * @param init_struct 初始化结构体的所有数据将被覆盖
     * @param mode 配置输入、输出、复用、模拟输入,以及推挽或开漏输出
     * @param pull 配置上拉、下拉或浮空
     * @param sp 配置速度
     * @param sh 配置输入施密特触发器
     */
    void init_pin(PinToken pin, GPIO_InitTypeDef& init_struct, pin_mode mode, pull_mode pull, speed sp, schmit sh) {
        init_struct.GPIO_Pin = pin.pin;
        init_struct.GPIO_Mode = static_cast<GPIOMode_TypeDef>(_GPIO_ENUM_TO_UNDERLYING(mode) & 0x00ff);
        init_struct.GPIO_Speed = _GPIO_ENUM_TO_ENUM(sp, GPIOSpeed_TypeDef);
        init_struct.GPIO_OType = static_cast<GPIOOType_TypeDef>(_GPIO_ENUM_TO_UNDERLYING(mode) >> 8);
        init_struct.GPIO_PuPd = _GPIO_ENUM_TO_ENUM(pull, GPIOPuPd_TypeDef);
        init_struct.GPIO_Schmit = _GPIO_ENUM_TO_ENUM(sh, GPIOSchmit_TypeDef);
        GPIO_Init(_GPIO_PORT_TO_POINTER(pin.port), &init_struct);
    }


    /**
     * @brief 复用已经配置好的GPIO_TypeDef 结构体继续配置引脚
     *
     * 使用方法是先用带配置参数的init_pin 重载把初始化结构体配置好,然后复用该结构体,调用这个函数配置其他参数相同的引脚
     *
     * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
     * @param init_struct 只修改初始化结构体的GPIO_Pin,其他保持原样
     */
    void init_pin(PinToken pin, GPIO_InitTypeDef& init_struct) {
        init_struct.GPIO_Pin = pin.pin;
        GPIO_Init(_GPIO_PORT_TO_POINTER(pin.port), &init_struct);
    }


    /**
     * @brief 内部创建一个GPIO_TypeDef 结构体,返回后丢弃
     *
     * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
     * @param init_struct 初始化结构体的所有数据将被覆盖
     * @param mode 配置输入、输出、复用、模拟输入,以及推挽或开漏输出
     * @param pull 配置上拉、下拉或浮空,默认为上拉
     * @param sp 配置速度,默认为中速/10MHz
     * @param sh 配置输入施密特触发器,默认使能
     */
    void init_pin(PinToken pin, pin_mode mode, pull_mode pull = pull_mode::up, speed sp = speed::high, schmit sh = schmit::enable) {
        auto init_struct = make_empty_init();
        init_pin(pin, init_struct, mode, pull, sp, sh);
    }


    inline static void set_pin_mode(PinToken pin, pin_mode mode) {
        _GPIO_PORT_TO_POINTER(pin.port)->MODER &= ~(GPIO_MODER_MODER0 << (pin.pin_source * 2));
        _GPIO_PORT_TO_POINTER(pin.port)->MODER |= ((_GPIO_ENUM_TO_UNDERLYING(mode) & 0x00ff) << (pin.pin_source * 2));
        if (mode == pin_mode::out_od || mode == pin_mode::out_pp || mode == pin_mode::af_od || mode == pin_mode::af_pp) {
            _GPIO_PORT_TO_POINTER(pin.port)->OTYPER &= ~((GPIO_OTYPER_OT_0) << pin.pin_source);
            _GPIO_PORT_TO_POINTER(pin.port)->OTYPER |= static_cast<uint16_t>((_GPIO_ENUM_TO_UNDERLYING(mode) >> 8) << (pin.pin_source));
        }
    }


    inline static auto test_pin(PinToken pin) {
        return _GPIO_PORT_TO_POINTER(pin.port)->IDR & pin.pin;
    }

    inline static void setpin(PinToken pin) {
        _GPIO_PORT_TO_POINTER(pin.port)->BSRR = pin.pin;
    }


    inline static void clrpin(PinToken pin) {
        _GPIO_PORT_TO_POINTER(pin.port)->BRR = pin.pin;
    }


    inline static void write_pin(PinToken pin, uint8_t level) {
        if (level == 0) {
            clrpin(pin);
        }
        else {
            setpin(pin);
        }
    }


    inline static void toggle_pin(PinToken pin) {
        _GPIO_PORT_TO_POINTER(pin.port)->ODR ^= pin.pin;
    }

    // TODO: AF config


    class ChainInit {
       private:
        GPIO_InitTypeDef init_struct;

       public:
        /**
         * @brief 修改初始化结构体并配置引脚,返回值可以链式调用继续初始化下一个引脚
         *
         * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
         * @param init_struct 初始化结构体的所有数据将被覆盖
         * @param mode 配置输入、输出、复用、模拟输入,以及推挽或开漏输出
         * @param pull 配置上拉、下拉或浮空,默认为上拉
         * @param sp 配置速度,默认为高速/10MHz
         * @param sh 配置输入施密特触发器,默认使能
         * @return ChainInit&
         */
        inline ChainInit& init(PinToken pin, pin_mode mode, pull_mode pull = pull_mode::up, speed sp = speed::high, schmit sh = schmit::enable) {
            gpio::init_pin(pin, this->init_struct, mode, pull, sp, sh);
            return *this;
        }


        /**
         * @brief 复用已经配置好的GPIO_TypeDef 结构体继续配置引脚
         *
         * @param pin 指向GPIO 引脚的PinToken,应该使用定义好的PAx 等常量
         * @return ChainInit&
         */
        inline ChainInit& init(PinToken pin) {
            gpio::init_pin(pin, this->init_struct);
            return *this;
        }
    };

}  // namespace gpio


  MAIN  ///


void nrst_pin_switch_as_pa0() {
    // 将NRST 复用为PA0
    RCC->APB1ENR |= RCC_APB1ENR_IOMUXEN;
    GPIOMUX->NRST_PIN_KEY = 0x5AE1;
    GPIOMUX->NRST_PA0_SEL = 1;
}


void Delay(uint32_t nCount)  // 简单的延时函数
{
    for (; nCount != 0; nCount--)
        __NOP();
}


constexpr auto LED0 = gpio::PA0;
constexpr auto LED1 = gpio::PC7;
constexpr auto LED2 = gpio::PD7;

int main(void) {
    // 测试PA0 和PD7 能不能用
    nrst_pin_switch_as_pa0();

    // LED 端口初始化
    gpio::enable_clk(LED0, LED1, LED2);

    // 设置三个LED 引脚为推挽输出
    gpio::ChainInit()
        .init(LED0, gpio::pin_mode::out_pp)
        .init(LED1)
        .init(LED2);

    using namespace gpio;

    while (1) {
        clrpin(LED0);  // 亮
        Delay(0x0FFFFF);
        setpin(LED0);  // 灭

        clrpin(LED1);  // 亮
        Delay(0x0FFFFF);
        setpin(LED1);  // 灭

        clrpin(LED2);  // 亮
        Delay(0x0FFFFF);
        setpin(LED2);  // 灭
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/429910.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

迭代器与仿函数

迭代器与仿函数一般分类功能方式分类STL迭代器的类型迭代器辅助函数流型迭代器仿函数仿函数的编写标准库中的仿函数一般分类 正向迭代器 容器名&#xff1a;iterator it begin() end() 2.反向迭代器 容器名&#xff1a;reverse_iterator it rbegin() rend() 3.常正向迭代器 容器…

MQTT 安全解析:构建可靠的物联网系统

物联网逐渐渗透到医疗保健、智能家居、智慧城市、自动驾驶等我们生活中的各个领域。这其中所涉及到的物联设备的安全也因此变得愈发重要。一旦物联网系统遭到恶意入侵&#xff0c;不仅海量设备数据将面临丢失、被窃取和篡改等安全风险&#xff0c;使用这些设备和物联网应用的终…

Githubs的使用方法(创建仓库\分支\提交【增删改查】\拉取与合并\管理与clone代码\修改分支等操作)

Githubs的使用方法 一、github基本使用 这一小节主要介绍github的基本使用方法以及每一步的流程和作用。 1. 创建仓库 2. 创建分支 此时有两个分支&#xff1a;main 和 readme-edits。 现在&#xff0c;它们看起来完全相同。 接下来&#xff0c;将向新分支添加更改。 3. 创…

Vue3 项目实例(一)ElementPlus+ pinia+vite创建

项目搭建 热重载&#xff1a;将一个项目切分成多个JS&#xff0c;同时利用浏览器的协商缓存。 etag: 文件唯一标识 如果某一片代码没有改变&#xff0c;devServer返回304&#xff0c;浏览器继续使用原来的文件&#xff0c;否则&#xff0c;返回200&#xff0c;响应新的js文件…

RK3568平台开发系列讲解(调试篇)IS_ERR函数的使用

🚀返回专栏总目录 文章目录 一、IS_ERR函数用法二、IS_ERR函数三、内核错误码沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍 IS_ERR 函数的使用。 一、IS_ERR函数用法 先看下用法: 二、IS_ERR函数 对于任何一个指针来说,必然存在三种情况: 一种是合…

知识图谱:Neo4j数据库的基本使用——创建张学良的关系谱

一、知识图谱及Neo4j数据库介绍 知识图谱&#xff08;Knowledge Graph&#xff09;是人工智能的重要分支技术&#xff0c;它在2012年由谷歌提出&#xff0c;是结构化的语义知识库&#xff0c;用于以符号形式描述物理世界中的概念及其相互关系&#xff0c;其基本组成单位是“实体…

4.1派生类的概念

&#xff1a;为什么使用继承 所谓继承就是从先辈处得到属性和行为特征。类的继承就是新的类从已有类那里得到已有特征。这样做的目的是&#xff1a;减少代码的重复。 派生类的声明 声明派生类的一般公式 &#xff1a; class 派生类名:[继承方式] 基类名 { 派生类新增的数据成…

Java并发基石_CAS原理实战02_CAS实现原理

文章目录什么是CAS&#xff1f;CAS的实现原理是什么&#xff1f;cmpxchg指令怎么保证多核心下的线程安全&#xff1f;什么是ABA问题&#xff1f;如何解决ABA问题呢&#xff1f;什么是CAS&#xff1f; CAS&#xff0c;全称CompareAndSwap&#xff0c;比较并替换。 CAS包含了三个…

MyBatis --- 缓存、逆向工程、分页插件

一、MyBatis的缓存 1.1、MyBatis的一级缓存 一级缓存是SqlSession级别的&#xff0c;通过同一个SqlSession查询的数据会被缓存&#xff0c;下次查询相同的数据&#xff0c;就会从缓存中直接获取&#xff0c;不会从数据库重新访问 使一级缓存失效的四种情况&#xff1a; 1、…

MySQL高级第十二篇:数据库事物概述和隔离级别

MySQL高级第十二篇&#xff1a;数据库事物概述和隔离级别一、数据库事物概述1. SHOW ENGINES 查看存储引擎2. 事物ACID特性原子性&#xff08;atomiity&#xff09;一致性&#xff08;consistency&#xff09;&#xff1a;隔离性&#xff08;isolation&#xff09;持久性&#…

使用java实现自动扫雷

写在前面 本项目已在github开源&#xff0c;链接https://github.com/QZero233/JavaAutoMinesweeper 本文的写作风格可能会有些奇怪&#xff0c;这是笔者的一次全新的尝试&#xff0c;后续会换回写blog的文风的 摘要 本文提出了一个全自动完成扫雷游戏的解决方案&#xff0c;…

【Kubernetes】 多云管理策略解析

文章目录Kubernetes 多云的实现1. 前言1.1 Kubernetes 多云的现实需求2. Kubernetes 多云的架构设计2.1 跨云 Kubernetes 的挑战2.1.1 不同云厂商的接口不兼容2.1.2 多云环境中的安全问题2.1.3 跨云环境中的网络问题2.2 Kubernetes 多云的架构设计2.2.1 统一网络管理2.2.2 使用…

新能源汽车高压配电管理(PDU/BDU)

一、概念与组成 PDU(Power Distribution Unit)&#xff0c;即高压配电单元&#xff0c;功能是负责新能源车高压系统中的电源分配与管理&#xff0c;为整车提供充放电控制、高压部件上电控制、电路过载短路保护、高压采样、低压控制等功能&#xff0c;保护和监控高压系统的运行…

MacOS系统启动React前端项目时报错Error: EMFILE: too many open files, open解决方法

错误场景 最近在开发React的前端微应用&#xff0c;启动时模块构建报错Module build failed&#xff0c; Error: EMFILE: too many open files, 如下图所示&#xff1a; Error: EMFILE: too many open files的错误&#xff0c;经排查是因为单个微应用项目较大&#xff0c;发…

【Linux安装数据库】Ubuntu安装mysql并连接navicat

Linux系统部署Django项目 文章目录Linux系统部署Django项目一、mysql安装二、mysql配置文件三、新建数据库和用户四、nivacat链接mysql一、mysql安装 linux安装mysql数据库有很多教程&#xff0c;根据安装方式不同&#xff0c;相关的步骤也不同。可以参考&#xff1a;【Linux安…

前端基础(HTML、CSS、JS、jQuery)

文章目录一、HTML基础1.1 常用标签&#xff08;表格、表单、按钮等&#xff09;1.2 其他一些标签&#xff08;书签、显示效果、缩写等&#xff09;二、CSS基础2.1 CSS引入方式2.2 CSS选择器2.3 CSS常用属性三、JavaScript3.1 JS使用方式3.2 变量和数据类型3.3 函数、作用域、条…

Unity基础框架从0到1(五)延时任务调度模块

索引 这是Unity基础框架从0到1的第五篇文章&#xff0c;前面的文章和对应的视频我一起列到这里&#xff1a; 文章 Unity基础框架从0到1 开篇 Unity游戏框架从0到1 (二) 单例模块 Unity基础框架从0到1&#xff08;三&#xff09;高效的全局消息系统 Unity基础框架从0到1&a…

CentOS 7 DNS服务器架设

CentOS 7 DNS服务器部署 项目背景和要求 要保证即能够解析内网域名linuxidc.local的解析&#xff0c;又能解析互联网的域名。 主DNS服务器&#xff1a;ZZYH1.LINUXIDC.LOCAL 辅助DNS服务器&#xff1a;ZZYH2.LINUXIDC.LOCAL 包含以下域的信息&#xff1a; 1、linuxidc.lo…

mybatis多表联查(一对一、一对多/多对一、多对多)

mybatis多表联查(一对一、一对多/多对一、多对多) 在开发过程中单表查询往往不能满足需求分析的很多功能&#xff0c;对于比较复杂业务来说&#xff0c;关联的表有几个&#xff0c;甚至是几十个并且表与表之间的关联相当复杂。为了能够实现复杂的功能业务&#xff0c;就必须进…

Java 并发工具合集 JUC 大爆发!!!

并发工具类 通常我们所说的并发包也就是 java.util.concurrent (JUC)&#xff0c;集中了 Java 并发的各种工具类&#xff0c; 合理地使用它们能帮忙我们快速地完成功能 。 1. CountDownLatch CountDownLatch 是一个同步计数器&#xff0c;初始化的时候 传入需要计数的线程等待数…