• 杰理AC632N蓝牙芯片iokey使用解析(通用MCU版)


    杰理AC632N系列是一款很好的蓝牙SOC,于2021年推出,同时也公开了SDK,对于开发者来说应该足够用了。以下内容基于杰理官方SDK(fw-AC63_GP_MCU-AC63_GP_MCU_v1.4.0)梳理总结。

    1.使用iokey前所需工作

    在用户main函数里初始化iokey,该函数位于apps/main.c文件。

    int user_main()
    {
    #if (USE_KEY_DRIVER == 1)			//使用IOkey
        key_driver_init(&iokey_para);	//初始化IOkey
    #endif
    
    #if (DUAL_BANK_UPDATE_BY_UFW)//使用双备份升级方式
        dual_bank_update_init();
    #endif
    
        while (1) {
            wdt_clear();//喂狗
    
            user_msg_handler();//处理消息,包含iokey按键消息
    
    //        __asm__ volatile("idle");//__asm__是GCC 关键字asm 的宏定义 #define __asm__ asm   用来声明一个内联汇编表达式
        }
    //__volatile__是GCC 关键字volatile 的宏定义   #define __volatile__ volatile  向GCC 声明不允许对该内联汇编优化,否则当 使用了优化选项(-O)进行编译时,GCC 将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    key_driver_init(&iokey_para); 用来初始化IOkey的各种配置,该函数位于bsp/AC632N/key_driver.c文件。注意看一下该函数的参数&iokey_para,该参数是一个结构体指针,其成员中.get_value .key_init .key_process是三个函数指针,该参数明确了具体指向的函数实体分别是io_get_key_value()、io_key_init(),io_key_process()。下面代码里列出该结构体原型以及三个函数原型。

    /*----------------------------------------------------------------------------*/
    /**@brief   按键初始化
       @param   key_para:按键参数
       @return  -1:初始化错误
                0:初始化成功
       @note
    */
    /*----------------------------------------------------------------------------*/
    int key_driver_init(struct key_driver_para *key_para)
    {
        //初始化
        if (key_para->key_init) {
            return key_para->key_init(key_para);
        }
    
        return -1;
    }
    
    
    //注意扫描频率,这里供的时间默认为10ms扫描一次
    //则对应的消抖时间为 4*10 =40ms,其他以此类推
    struct key_driver_para iokey_para = {
        .last_key 		  = NO_KEY,  		    //上一次get_value按键值, 初始化为NO_KEY;
        .filter_time  	  = 4,				    //按键消抖延时;
        .long_time 		  = 75,  			    //按键判定长按数量
        .hold_time 		  = (75 + 15),  	    //按键判定HOLD数量
        .get_value 		  = io_get_key_value,   //按键值获取
        .key_init         = io_key_init,        //按键状态初始化
        .key_process      = io_key_process,     //按键处理
        .key_init_ok      = 0,                  //按键初始化完成标志
    };
    //上面的结构体定义的原型在对应的key_driver.h头文件里,具体如下:
    struct key_driver_para {
        u8  last_key;  	         //上一次get_value按键值
    //== 用于消抖类参数
        u8 filter_value; 		 //用于按键消抖
        u8 filter_cnt;  		 //用于按键消抖时的累加值
        u8 filter_time;	         //当filter_cnt累加到base_cnt值时, 消抖有效
    //== 用于判定长按和HOLD事件参数
        u8 long_time;  	        //按键判定长按数量
        u8 hold_time;  	        //按键判定HOLD数量
        u8 press_cnt;  	        //与long_time和hold_time对比, 判断long和hold状态
    
        u8(*get_value)(void);                                  //按键值获取
        int (*key_init)(void *key_para);                       //按键初始化
        void (* key_process)(u8 key_status, u8 key_value);     //按键处理
        u8 key_init_ok;                                        //按键初始化完成标志
    };
    //----------------------------------------------------------
    //结构体内三个函数指针成员的原型:
    int io_key_init(void *para)
    {
        struct key_driver_para *scan_para = (struct key_driver_para *)para;
    
        gpio_set_direction(io_key_table[0], 1);//设置io_key_table[0]引脚为输入
        gpio_set_direction(io_key_table[1], 1);//设置io_key_table[1]引脚为输入
    
        gpio_set_pull_down(io_key_table[0], 0);//设置io_key_table[0]引脚为不下拉
        gpio_set_pull_down(io_key_table[1], 0);//设置io_key_table[1]引脚为不下拉
    
        gpio_set_pull_up(io_key_table[0], 1);//设置io_key_table[0]引脚为上拉
        gpio_set_pull_up(io_key_table[1], 1);//设置io_key_table[1]引脚为上拉
    
        gpio_set_die(io_key_table[0], 1);//设置io_key_table[0]引脚为数字功能引脚(非模拟电压adc之类功能)
        gpio_set_die(io_key_table[1], 1);//设置io_key_table[1]引脚为数字功能引脚(非模拟电压adc之类功能)
    
        scan_para->key_init_ok = 1;  //标志置位,显示初始化过了
        puts("___io_key_init_ok___");
        return 0;
    }
    
    /*----------------------------------------------------------------------------*/
    /**@brief   IO按键值获取
       @param   void
       @return  按键值
       @note
    */
    /*----------------------------------------------------------------------------*/
    u8 io_get_key_value(void)
    {
        //按键接地为按下
        if (!gpio_read(io_key_table[0])) {
            return 0;
        } else if (!gpio_read(io_key_table[1])) {
            return 1;
        }
        return NO_KEY;
    }
    
    /*----------------------------------------------------------------------------*/
    /**@brief   按键处理函数
       @param   key_status:  按键状态(短按、长按、连按、抬起等)
       @param   key_value:   按键值
       @return  viod
       @note    注意按键处理时间不宜过长
    */
    /*----------------------------------------------------------------------------*/
    void io_key_process(u8 key_status, u8 key_value)
    {
        //按键处理,中断时间不宜过久,这里仅仅是作按键的区分
        //建议依靠消息机制把按键信息发送到主循环处理按键
        u16 key_msg;
        u8 err;
        key_msg = io_key_msg_table[key_value][key_status];//根据键值和按键状态在二维数组里找出对应的消息值。key_msg消息值是一个枚举类型中的某个元素值。
        err = msg_put_fifo(key_msg);//把按键消息放进消息池,先进先出方式
        if (err != 0) {
            puts("can not put msg");//串口打印消息
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111

    初始化函数里用到了一个数组,io_key_table[ ],该数组在bsp/AC632N/board/board_demo.c文件里定义,指明了具体要初始化的两个引脚,一个是PA1,一个是PA2,在板级别=定义应该是为了方便开发者修改。

    
    /************************** key driver config****************************/
    #if (USE_KEY_DRIVER == 1)
    //2个测试io按键
    u16 io_key_table[2] = {
        IO_PORTA_01,
        IO_PORTA_02,
    };
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.按键扫描

    扫描到按键有动作后,根据键值和键状态,找到对应的消息值,把该消息放到消息池中。

    /*----------------------------------------------------------------------------*/
    /**@brief   按键扫描函数,判断是否有按键按下,按下的时间状态
       @param   _scan_para:按键参数
       @return  viod
       @note
    */
    /*----------------------------------------------------------------------------*/
    void  key_driver_scan(void *_scan_para)
    {
        struct key_driver_para *scan_para = (struct key_driver_para *)_scan_para;
    
        u8 key_status = 0;
        u8 cur_key_value = NO_KEY;
        u8 key_value = NO_KEY;
    
        if (!scan_para->key_init_ok) {
            return;
        }
    
    //===== 按键值获取
        cur_key_value = scan_para->get_value();
        /* if (cur_key_value != NO_KEY) { */
        /*     log_info(">>>cur_key_value: %d", cur_key_value); */
        /* } */
    
    //===== 按键消抖处理
        if (cur_key_value != scan_para->filter_value && scan_para->filter_time) {	//当前按键值与上一次按键值如果不相等, 重新消抖处理, 注意filter_time != 0;
            scan_para->filter_cnt = 0; 		            //消抖次数清0, 重新开始消抖
            scan_para->filter_value = cur_key_value;	//记录上一次的按键值
            return; 		                            //第一次检测, 返回不做处理
        }
    
        if (scan_para->filter_cnt < scan_para->filter_time) {//当前按键值与上一次按键值相等, filter_cnt开始累加;
            scan_para->filter_cnt++;
            return;
        }
    
    //===== 按键类型判断
        if (cur_key_value == scan_para->last_key) {
            if (cur_key_value == NO_KEY) {                                     //没有按键按下
                return;
            }
            scan_para->press_cnt++;
            if (scan_para->press_cnt == scan_para->long_time) {                //长按
                key_status = KEY_LONG;
            } else if (scan_para->press_cnt == scan_para->hold_time) {         //连按(一直按着不放)
                key_status = KEY_HOLD;
                scan_para->press_cnt = scan_para->long_time;
            } else {
                return;//计数不做操作
            }
            key_value = cur_key_value;
        } else {
            if (cur_key_value == NO_KEY) {                                   //按键被抬起
                if (scan_para->press_cnt < scan_para->long_time) {           //短按
                    key_status = KEY_SHORT_UP;
                    key_value = scan_para->last_key;
                } else if (scan_para->press_cnt >= scan_para->long_time) {   //长按/HOLD状态之后按键抬起;
                    key_status = KEY_LONG_HLOD_UP;
                    key_value = scan_para->last_key;
                }
                scan_para->last_key = cur_key_value;
                scan_para->press_cnt = 0;
            } else {
                scan_para->last_key = cur_key_value;
                scan_para->press_cnt = 0;
                return;
            }
        }
    
    //====== 按键处理
        //按键处理,中断时间不宜过久,这里仅仅是作按键的区分
        //建议依靠消息机制把按键信息发送到主循环处理按键
        scan_para->key_process(key_status, key_value);//根据键值和键状态,找到对应的消息值,把该消息放到消息池中
    }
    
    //该函数内部调用了初始化时结构体内函数指针指定的两个函数:u8 io_get_key_value(void)和void io_key_process(u8 key_status, u8 key_value)
    
    /*----------------------------------------------------------------------------*/
    /**@brief   IO按键值获取
       @param   void
       @return  按键值
       @note
    */
    /*----------------------------------------------------------------------------*/
    u8 io_get_key_value(void)
    {
        //按键接地为按下
        if (!gpio_read(io_key_table[0])) {
            return 0;
        } else if (!gpio_read(io_key_table[1])) {
            return 1;
        }
        return NO_KEY;
    }
    
    /*----------------------------------------------------------------------------*/
    /**@brief   按键处理函数
       @param   key_status:  按键状态(短按、长按、连按、抬起等)
       @param   key_value:   按键值
       @return  viod
       @note    注意按键处理时间不宜过长
    */
    /*----------------------------------------------------------------------------*/
    void io_key_process(u8 key_status, u8 key_value)
    {
        //按键处理,中断时间不宜过久,这里仅仅是作按键的区分
        //建议依靠消息机制把按键信息发送到主循环处理按键
        u16 key_msg;
        u8 err;
        key_msg = io_key_msg_table[key_value][key_status];
        err = msg_put_fifo(key_msg);
        if (err != 0) {
            puts("can not put msg");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117

    io_key_process()函数里使用了消息数组,也就是所谓的按键消息表u16 io_key_msg_table[ ][ ]。它是一个二维数组,2个维度分别是键值和键状态。所谓键值,就是用来分辨哪一个引脚对应按键按下了,键状态就是按下了时间多久,用于区分短按、长按、连续按、抬起等。该数组定义在key_driver.c文件里。

    //io 按键消息表
    u16 io_key_msg_table[2][4] = {
        //短按                  //长按                 //连按(hold)           //长、连按抬起
        [0] = {
            MSG_TEST_IO_KEY1_SHORT, MSG_TEST_IO_KEY1_LONG, MSG_TEST_IO_KEY1_HOLD, MSG_TEST_IO_KEY1_LONG_HOLD_UP
        },
    
        [1] = {
            MSG_TEST_IO_KEY2_SHORT, MSG_TEST_IO_KEY2_LONG, MSG_TEST_IO_KEY2_HOLD, MSG_TEST_IO_KEY2_LONG_HOLD_UP
        },
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    按键消息表u16 io_key_msg_table[ ][ ]里每一个元素对应一个消息值,该消息值在msg.h文件定义,是一个枚举类型。

    //定义空消息
    #define   NO_MSG       0xffff
    
    
    //消息表
    enum {
    
        MSG_TEST_IO_KEY1_SHORT,
        MSG_TEST_IO_KEY1_LONG,
        MSG_TEST_IO_KEY1_HOLD,
        MSG_TEST_IO_KEY1_LONG_HOLD_UP,
    
        MSG_TEST_IO_KEY2_SHORT,
        MSG_TEST_IO_KEY2_LONG,
        MSG_TEST_IO_KEY2_HOLD,
        MSG_TEST_IO_KEY2_LONG_HOLD_UP,
    
    
        MSG_MAX = NO_MSG,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    scan_para->key_process(key_status, key_value),用来根据键值和键状态,从二维数组按键消息表中找到对应的消息值,把该消息值放到消息池中。其中调用的函数为u8 msg_put_fifo(u16 msg)。

    //消息池大小
    #define   MAX_POOL   10
    
    //临界区管理
    #define   MSG_ENTER_CRITICAL()    local_irq_disable()
    #define   MSG_EXIT_CRITICAL()     local_irq_enable()
    
    u16 msg_pool[MAX_POOL];             //消息池
    u16 msg_read = 0;                   //消息读位置
    u16 msg_write = 0;                  //消息写位置
    u16 msg_pool_residue = MAX_POOL;    //消息池剩余大小
    
    /*----------------------------------------------------------------------------*/
    /**@brief   先进先出的方式将消息放入消息池
       @param   void
       @return  0:消息放入消息池成功
                -1:消息放放入消息池不成功
       @note
    */
    /*----------------------------------------------------------------------------*/
    u8 msg_put_fifo(u16 msg)
    {
        MSG_ENTER_CRITICAL();//关闭中断
        if (msg_pool_residue == 0) {		//检查消息池子是否满了
            MSG_EXIT_CRITICAL();
            puts("err:msg pool is full!\n");
            return -1;
        }
        msg_pool[msg_write] = msg;		//把消息值放到消息池子
        msg_write++;
        if (msg_write == MAX_POOL) {
            msg_write = 0;
        }
        msg_pool_residue--;				//剩余容量-1
        MSG_EXIT_CRITICAL();		//打开中断
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    所谓消息池子,就是一个数组,u16 msg_pool[MAX_POOL];其中已经定义了MAX_POOL为10,也就是说最大只能装10个消息,如果再不取走,就停止新消息入池。取出消息需要使用u16 get_msg(void)函数。
    按键扫描函数你会发现再用户主函数里并没有该按键扫描函数,它被放在timer的中断服务函数里了。timer1定时器2ms中断一次,然后每隔5次就扫描一次按键。该中断服务函数如下:

    
    ___interrupt
    static void timer1_isr()
    {
        const struct timer_target *p;
    
        JL_TIMER1->CON |= BIT(14);
    
        list_for_each_timer_target(p) {
            if (p->timer_handle) {
                p->timer_handle();
            }
        }
    
        ++cnt;
    #if (USE_KEY_DRIVER == 1) //10ms 按键扫描
        if ((cnt % 5) == 0) {
            key_driver_scan(&iokey_para);
        }
    #endif
        if ((cnt % 50) == 0) {
            //100ms
        }
        if ((cnt % 100) == 0) {
            //200ms
        }
        if ((cnt % 250) == 0) {
            if (cnt == 500) {
                cnt = 0;
            }
            //500ms
    #if 0
            powerdown_sleep();
    #else
            putchar('h');//这里每隔500ms串口打印一个字符h
    #endif
        }
    
        /*lrc_scan();*/
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    3.消息获取

    从上面程序可以看出,按键扫描后把消息值推送到了消息池子了,开发者并不需要直接在按键扫描函数里做任何处理工作,只要在user_main主函数里从消息池子里循环取出消息值即可,然后根据该值做相应的处理工作。
    在主函数里循环取出消息并处理,使用的是user_msg_handler();函数,该函数被直接定义在user_main函数的上面。它使用了switch语句根据消息值做对应动作。
    该函数并不是只能处理按键消息,所有其他消息均可以放入消息池子,然后交由该函数处理。

    
    static void user_msg_handler()
    {
    #if (DUAL_BANK_UPDATE_BY_UFW)
        update_download_opt();
    #endif
    
    #if (USE_KEY_DRIVER == 1)
    
        int msg = get_msg();//从消息池子里取出一个消息
        if (msg == NO_MSG) {
            return;
        }
    
        switch (msg) {
        case MSG_TEST_IO_KEY1_SHORT://按键1短按
            log_info("IO_KEY1_SHOURT");
            break;
        case MSG_TEST_IO_KEY1_LONG:
            log_info("IO_KEY1___LONG");
            break;
        case MSG_TEST_IO_KEY1_HOLD:
            log_info("IO_KEY1_HOLD");
            break;
        case MSG_TEST_IO_KEY1_LONG_HOLD_UP:
            log_info("IO_KEY1_LONG_HOLD_UP");
            break;
        case MSG_TEST_IO_KEY2_SHORT://按键2短按
            log_info("IO_KEY2_SHOURT");
            break;
        case MSG_TEST_IO_KEY2_LONG:
            log_info("IO_KEY2___LONG");
            break;
        case MSG_TEST_IO_KEY2_HOLD:
            log_info("IO_KEY2_HOLD");
            break;
        case MSG_TEST_IO_KEY2_LONG_HOLD_UP:
            log_info("IO_KEY2_LONG_HOLD_UP");
            break;
        default:
            break;
        }
    
    #endif
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    消息处理函数调用了u16 get_msg(void)函数用于从消息池子里取出一个消息,该代码里消息池子使用的是先进先出方式。查看代码,可以发现官方还定义了一种先进后出的方式,该SDK未使用。

    /*----------------------------------------------------------------------------*/
    /**@brief   获取一个消息
       @param   void
       @return  消息值
       @note
    */
    /*----------------------------------------------------------------------------*/
    u16 get_msg(void)
    {
        u16 msg = NO_MSG;
    
        MSG_ENTER_CRITICAL();
        if (msg_pool_residue < MAX_POOL) {
            msg = msg_pool[msg_read];//从消息池子里取出一个消息
            msg_read++;
            if (msg_read == MAX_POOL) {
                msg_read = 0;
            }
            msg_pool_residue++;//消息取走一个,消息池子剩余容量+1
        }
        MSG_EXIT_CRITICAL();
        return msg;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    以上代码全部分析完了,可以看出官方SDK使用的是一种消息池的机制,用于各种事件的处理。该机制的好处在事件的处理很集中,可以进一步把消息发生源封装起了不对开发者可见,开发者只关心如何进行初始化和处理消息即可。另外还可以把消息池子升维,加上权重优先级等,这样各种形形色色的消息到了池子里,就按照优先级规则等进行取走,满足各种性质事件的处理需求。这就类似于DA14580的ke内核消息机制了。
    写的有点多了,苏州高温40℃,继续搬砖。

  • 相关阅读:
    (SCA)正弦余弦算法SCA: A Sine Cosine Algorithm(代码可复制粘贴)
    Oracle is和as 关键字学习
    swift 约束布局
    java项目-第160期ssm大学生校园兼职系统_ssm毕业设计_计算机毕业设计
    【每日一句】名人金句学英语(1130)
    strimzi实战之二:部署和消息功能初体验
    SpringCache缓存处理
    自动创建word文档的exe文件,自定义文件名、保存路径
    AUTOSAR汽车电子嵌入式编程精讲300篇-车载网络 CAN 总线报文异常检测
    hive修复所有表
  • 原文地址:https://blog.csdn.net/ydgd118/article/details/126220506