HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案

news2024/11/18 22:22:40

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案

    • 1. 项目介绍
    • 2. Keil安装
    • 3. 创建新项目
      • 3.1 参考博文
      • 3.2 流程
    • 4. 发送串口数据
      • 4.1 参考博文
      • 4.2 串口收发流程
    • 5. 产生波形
      • 5.1 头文件封装
      • 5.2 初始化GPIO口
      • 5.3 产生并观察方波
    • 6. Keil信号函数和中断
      • 6.1 中断初始化
      • 6.2 信号函数编写
      • 6.3 综合运行
    • 7. 总结

发牢骚:博主好久没有发博客了。实在因为读研和工作无二,缺少自己的时间,所有时间都得投入到科研项目中。一些自己的时间在准备创业项目,或是浅浅玩玩游戏放松一下心情。这次课程项目也当给点时间缓冲一下吧。

正文:2023年HITSZ嵌入式计算研究生课程项目(张春恺副教授授课)。项目主要使用Keil模拟器实现几个嵌入式系统中的基本功能,但麻烦点在于:

  1. Keil模拟器资料较少,国内外的文档不够,社区不够活跃,许多问题找不到解决方案,非常多的坑,本文帮助大家避免坑;
  2. 本课程不会介绍任何与Keil相关、嵌入式系统底层开发相关的问题,所有实现都需要自行搜索,从而加剧问题1;
  3. Keil UI界面太丑,开发起来很恶心,还好不用做大系统开发……

博主已将代码放在github以供参考,不多BB,直接开始讲解

1. 项目介绍

在这里插入图片描述
简单来说:

  1. 安装Keil
  2. 创建一个新项目
  3. 从Keil的模拟器启动
  4. 编写程序
    • 向串口发送数据
    • 输出周期波
    • 使用Keil中的信号函数触发中断

2. Keil安装

Keil的安装教程很多,国内的教程就够了,最重要的是要搞个注册机,不然会面临程序太大不让跑的情况。可以参考下面这个链接进行Keil的安装:(1条消息) Keil uVision5 5.38官方下载、安装及注册教程_keil uvision5下载_这是乐某的博客-CSDN博客

注意,虽然项目要求跑uV3,但目前最新版本是uV5,我们使用uV5即可。

3. 创建新项目

由于是从模拟器上跑,[正点原子](正点原子资料下载中心 — 正点原子资料下载中心 1.0.0 文档)提供的样例很难直接跑起来。考虑到要完成的任务比较简单,我们还是老老实实自己创建一个裸板项目。这个过程比较繁杂,参考了较多的文章,现提取两篇最有价值的罗列如下。

3.1 参考博文

  • 用操作系统直接启动(目标是STM32F103系列芯片,基本可以完成整个项目,但对了解底层帮助不大):不用板子也能跑!Keil模拟STM32F103体验 - 知乎 (zhihu.com)
  • 用ARMCM7启动(后面发现无法访问寄存器,千万不要死磕ARM Cortex-M系列芯片):创建keil仿真工程(ARM内核)_繁华一世终的博客-CSDN博客

3.2 流程

博主在测试验证中,发现ARMCM7有巨多坑,比如,[ARM官网](Documentation – Arm Developer)给出的UART布局(即0x16000000UART0的起始地址)根本无法在模拟器上访问。因此,在挣扎半天后放弃使用ARMCM7,而使用STM32F103RE芯片(STM32F103CB应该也可以,这里使用STM32F103RE主要想用STM32F103xE.h头文件中定义的DAC模电转换模块,然而发现模拟器上似乎并不能使用)。

下面正式介绍具体创建流程:

  • 打开Keil uV5,点击工具栏的Project,下拉选择new uVision Project
    在这里插入图片描述

  • 随便保存在一个文件夹(博主创建了一个空的Demo文件夹)中后选择STM32F103CB(这里和STM32F103RE不一样,但问题不大)
    在这里插入图片描述

  • 弹出来的对话框是安装运行时库,其实就是提供一些模块化代码。参考创建keil仿真工程(ARM内核)_繁华一世终的博客-CSDN博客,勾选CMSIS中的COREDevice中的Startup
    在这里插入图片描述

  • 进入如下界面(点加号把Target 1展开就能看到),可以对各种文件夹重命名,这里博主把Source Group 1命名为Src:
    在这里插入图片描述

  • 右键添加新文件,我们要添加一个main.c
    在这里插入图片描述

  • 接下来,我们把这位大哥(创建keil仿真工程(ARM内核)_繁华一世终的博客-CSDN博客)的测试程序贴下来,放到main.c

    //标志位
    char flag = 0;
    //延时
    void delay(int count)
    {
    	for(; count; count--);
    }
    //测试函数
    void function_Test(void)
    {
    	if(flag)			//翻转标志位
    		flag = 0;
    	else
    		flag = 1;
    	delay(1000);		//延时
    }
    int main(void)
    {
    	while(1)
    	{
    		function_Test();
    	}
    }
    

    F7编译,应该没有错误

  • 接下来设置模拟器选项,点击菜单栏中的Options for Target魔术棒
    在这里插入图片描述

  • 切换到Debug栏,做如下修改,点击OK关闭对话框(注意,如果选的STM32F103xx,那后面的Parameter就一定要填STM32F103xx
    在这里插入图片描述

  • 点击Debug按钮,进入Debug模式
    在这里插入图片描述

  • flag变量加入逻辑分析器中,按下F5,全速运行
    在这里插入图片描述

  • 点击Logic Analyzer中的auto以自动缩放比例
    在这里插入图片描述

至此,恭喜你,我们完成了新项目的创建。

4. 发送串口数据

这一步一定要保证前面创建工程时都严格照做。不然可能出现多种问题。这里主要强调两个点:

  • 一定不要选择ARMCM7芯片,选择STM32F103系列
  • 点击魔术棒切换到Debug栏后一定要选择simulator模式,并且下方Dialog DLLParameter一定要改对;

网上有许多介绍STM32串口编程的博客,我们更希望通过直接操作地址来实现对串口的收发,因为这样对我们理解底层更有帮助。网上还有许多HAL库的版本,这些库依赖底层BSP实现,构建起来不容易,作为课程项目,熟悉寄存器操作就好了。

4.1 参考博文

  • 这一篇就够了:(1条消息) STM32裸机开发(5) — 在Keil-MDK下编写uart串口打印程序_keil中usartreceive怎么写_Willliam_william的博客-CSDN博客
  • 把串口通信皮都扒了:(1条消息) STM32通过串口通信(汇编)_串口汇编_伊始不觉的博客-CSDN博客
  • 这是ARM的,不用参考了,地址不对:(1条消息) ARM裸板开发——UART通信方式及使用_跑不了的你的博客-CSDN博客

4.2 串口收发流程

创建Inc文件夹(Add New Group)用于存放头文件,此后,创建Src/uart.cInc/uart.h两个文件,内容如下(参考自(1条消息) STM32裸机开发(5) — 在Keil-MDK下编写uart串口打印程序_keil中usartreceive怎么写_Willliam_william的博客-CSDN博客):

// Src/uart.c


#include "uart.h"

typedef unsigned int uint32_t;
typedef struct
{
  volatile uint32_t SR;    /*!< USART Status register, Address offset: 0x00 */
  volatile uint32_t DR;    /*!< USART Data register,   Address offset: 0x04 */
  volatile uint32_t BRR;   /*!< USART Baud rate register, Address offset: 0x08 */
  volatile uint32_t CR1;   /*!< USART Control register 1, Address offset: 0x0C */
  volatile uint32_t CR2;   /*!< USART Control register 2, Address offset: 0x10 */
  volatile uint32_t CR3;   /*!< USART Control register 3, Address offset: 0x14 */
  volatile uint32_t GTPR;  /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;


void uart_init(void)
{
	USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
	volatile unsigned int *pReg;
	/* 使能GPIOA/USART1模块 */
	/* RCC_APB2ENR */
	pReg = (volatile unsigned int *)(0x40021000 + 0x18);
	*pReg |= (1<<2) | (1<<14);
	
	/* 配置引脚功能: PA9(USART1_TX), PA10(USART1_RX) 
	 * GPIOA_CRH = 0x40010800 + 0x04
	 */
	pReg = (volatile unsigned int *)(0x40010800 + 0x04);
	
	/* PA9(USART1_TX) */
	*pReg &= ~((3<<4) | (3<<6));
	*pReg |= (1<<4) | (2<<6);  /* Output mode, max speed 10 MHz; Alternate function output Push-pull */

	/* PA10(USART1_RX) */
	*pReg &= ~((3<<8) | (3<<10));
	*pReg |= (0<<8) | (1<<10);  /* Input mode (reset state); Floating input (reset state) */
	
	/* 设置波特率
	 * 115200 = 8000000/16/USARTDIV
	 * USARTDIV = 4.34
	 * DIV_Mantissa = 4
	 * DIV_Fraction / 16 = 0.34
	 * DIV_Fraction = 16*0.34 = 5
	 * 真实波特率:
	 * DIV_Fraction / 16 = 5/16=0.3125
	 * USARTDIV = DIV_Mantissa + DIV_Fraction / 16 = 4.3125
	 * baudrate = 8000000/16/4.3125 = 115942
 	 */
#define DIV_Mantissa 4
#define DIV_Fraction 5
	usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction);
	
	/* 设置数据格式: 8n1 */
	usart1->CR1 = (1<<13) | (0<<12) | (0<<10) | (1<<3) | (1<<2);	
	usart1->CR2 &= ~(3<<12);
	
	/* 使能USART1 */
}
	
int getchar(void)
{
	USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
	while ((usart1->SR & (1<<5)) == 0);
	return usart1->DR;
}

int putchar(char c)
{
	USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
	while ((usart1->SR & (1<<7)) == 0);
	usart1->DR = c;
	
	return c;
}
// Inc/uart.h

#ifndef _UART_H
#define _UART_H

void uart_init(void);
int getchar(void);
int putchar(char c);

#endif

修改main.c如下:

// Src/main.c

#include "uart.h"

...

int main(void)
{
	uart_init(); // 初始化串口
    
    // putchar
	putchar('D');
	putchar('e');
	putchar('a');
	putchar('d');
	putchar('p');
	putchar('o');
	putchar('o');
	putchar('l');
	putchar('m');
	putchar('i');
	putchar('n');
	putchar('e');
	putchar('\n');
	putchar('\r');
	...
}

点击Debug,并唤出串口,这里选择UART #1即可,因为参考的文章只启动了UART1.
在这里插入图片描述

F5全速运行,结果如下图所示,可以看到,下方UART #1串口窗口出现了Deadpoolloveshisstar
在这里插入图片描述

至此,恭喜你,完成了第一题

5. 产生波形

事实上,我曾一度以为这位大哥的博客:创建keil仿真工程(ARM内核)_繁华一世终的博客-CSDN博客已经完成了波形发生任务,但是LJJ告诉我应该把波形向IO口上发……得嘞,那就研究一下GPIO了。

5.1 头文件封装

在串口收发的实现中,我们直接操作一系列硬件地址,例如:0x40013800等。那么GPIO口的地址是多少呢?为了便于查阅,我们引入几个已经封装好的头文件:

  • stm32f1xx.h
  • stm32f103xb.h
  • system_stm32f1xx.h

找这些头文件是一件很麻烦的事情,博主首先下载了不用板子也能跑!Keil模拟STM32F103体验 - 知乎 (zhihu.com)这个操作系统源码,源码目录下rtthread_simulator_v0.1.0\\Libraries\\CMSIS\\Device\\ST\\STM32F1xx\\Include里可以找到这几个(也可参考博主的项目,但是我用的是stm32f103xe.h,可能你还得自己找找,代码贴不出来,太多了……)。

在这里插入图片描述

接下来,我们首先把这几个文件拷贝到Inc目录下,接下来通过Add Exisiting Files将这几个头文件放到Inc目录下

在这里插入图片描述

现在,点击stm32f103xb.h头文件,全局搜索就可找到之前对USART1(其实就是UART1)的地址定义,计算可知,这个地址与我们在发送数据至串口部分地址一样。同时,这里我们还能够看到一堆GPIO口的定义,例如GPIOA_BASE等。

在这里插入图片描述

可以看到在stm32f1xx.h有这样一段代码:

#if defined(STM32F100xB)
  #include "stm32f100xb.h"
#elif defined(STM32F100xE)
  #include "stm32f100xe.h"
#elif defined(STM32F101x6)
  #include "stm32f101x6.h"
#elif defined(STM32F101xB)
  #include "stm32f101xb.h"
#elif defined(STM32F101xE)
  #include "stm32f101xe.h"
#elif defined(STM32F101xG)
  #include "stm32f101xg.h"
#elif defined(STM32F102x6)
  #include "stm32f102x6.h"
#elif defined(STM32F102xB)
  #include "stm32f102xb.h"
#elif defined(STM32F103x6)
  #include "stm32f103x6.h"
#elif defined(STM32F103xB)
  #include "stm32f103xb.h"
#elif defined(STM32F103xE)
  #include "stm32f103xe.h"
#elif defined(STM32F103xG)
  #include "stm32f103xg.h"
#elif defined(STM32F105xC)
  #include "stm32f105xc.h"
#elif defined(STM32F107xC)
  #include "stm32f107xc.h"
#else
 #error "Please select first the target STM32F1xx device used in your application (in stm32f1xx.h file)"
#endif

这意味着要定义设备才能编译通过,具体操作时打开魔术棒(Options for Target),然后选择C/C++栏,在Define处添加对应设备即可:
在这里插入图片描述

5.2 初始化GPIO口

多亏博主之前摸索过正点原子的板子,知道他们有一堆例程可以参考,因此果断下载了STM32F103的开发例程:stm32f103战舰开发板V4 — 正点原子资料下载中心 1.0.0 文档。主要参考4,程序源码/1,标准例程-寄存器版本这个文件夹里面的项目。其中,博主参考实验3 按键输入实验完成了对GPIOA口的初始化,具体来说就是把GPIOA口的一个管脚(我选的4)配置为输出模式,使能GPIOA口的时钟。为了减少代码编写量,博主直接将正点原子的sys.csys.h文件拷贝到项目中,为了方便大家实验,直接贴出两个文件如下:

// Src/sys.c

/**
 ****************************************************************************************************
 * @file        sys.c
 * @author      正点原子团队(ALIENTEK)
 * @version     V1.1
 * @date        2020-04-17
 * @brief       系统初始化代码(包括时钟配置/中断管理/GPIO设置等)
 * @license     Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
 ****************************************************************************************************
 * @attention
 *
 * 实验平台:正点原子 STM32F103开发板
 * 在线视频:www.yuanzige.com
 * 技术论坛:www.openedv.com
 * 公司网址:www.alientek.com
 * 购买地址:openedv.taobao.com
 *
 * 修改说明
 * V1.0 20200417
 * 第一次发布
 *
 * V1.1 20221031
 * 在sys_stm32_clock_init函数添加相关复位/置位代码,关闭非必要外设,避免部分例程异常
 *
 ****************************************************************************************************
 */

// 这里我改成了sys.h
#include "sys.h"


/**
 * @brief       设置中断向量表偏移地址
 * @param       baseaddr: 基址
 * @param       offset: 偏移量(必须是0, 或者0X100的倍数)
 * @retval      无
 */
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset)
{
    /* 设置NVIC的向量表偏移寄存器,VTOR低9位保留,即[8:0]保留 */
    SCB->VTOR = baseaddr | (offset & (uint32_t)0xFFFFFE00);
}

/**
 * @brief       设置NVIC分组
 * @param       group: 0~4,共5组, 详细解释见: sys_nvic_init函数参数说明
 * @retval      无
 */
static void sys_nvic_priority_group_config(uint8_t group)
{
    uint32_t temp, temp1;
    temp1 = (~group) & 0x07;/* 取后三位 */
    temp1 <<= 8;
    temp = SCB->AIRCR;      /* 读取先前的设置 */
    temp &= 0X0000F8FF;     /* 清空先前分组 */
    temp |= 0X05FA0000;     /* 写入钥匙 */
    temp |= temp1;
    SCB->AIRCR = temp;      /* 设置分组 */
}

/**
 * @brief       设置NVIC(包括分组/抢占优先级/子优先级等)
 * @param       pprio: 抢占优先级(PreemptionPriority)
 * @param       sprio: 子优先级(SubPriority)
 * @param       ch: 中断编号(Channel)
 * @param       group: 中断分组
 *   @arg       0, 组0: 0位抢占优先级, 4位子优先级
 *   @arg       1, 组1: 1位抢占优先级, 3位子优先级
 *   @arg       2, 组2: 2位抢占优先级, 2位子优先级
 *   @arg       3, 组3: 3位抢占优先级, 1位子优先级
 *   @arg       4, 组4: 4位抢占优先级, 0位子优先级
 * @note        注意优先级不能超过设定的组的范围! 否则会有意想不到的错误
 * @retval      无
 */
void sys_nvic_init(uint8_t pprio, uint8_t sprio, uint8_t ch, uint8_t group)
{
    uint32_t temp;
    sys_nvic_priority_group_config(group);  /* 设置分组 */
    temp = pprio << (4 - group);
    temp |= sprio & (0x0f >> group);
    temp &= 0xf;                            /* 取低四位 */
    NVIC->ISER[ch / 32] |= 1 << (ch % 32);  /* 使能中断位(要清除的话,设置ICER对应位为1即可) */
    NVIC->IP[ch] |= temp << 4;              /* 设置响应优先级和抢断优先级 */
}

/**
 * @brief       外部中断配置函数, 只针对GPIOA~GPIOG
 * @note        该函数会自动开启对应中断, 以及屏蔽线
 * @param       p_gpiox: GPIOA~GPIOG, GPIO指针
 * @param       pinx: 0X0000~0XFFFF, 引脚位置, 每个位代表一个IO, 第0位代表Px0, 第1位代表Px1, 依次类推. 比如0X0101, 代表同时设置Px0和Px8.
 *   @arg       SYS_GPIO_PIN0~SYS_GPIO_PIN15, 1<<0 ~ 1<<15
 * @param       tmode: 1~3, 触发模式
 *   @arg       SYS_GPIO_FTIR, 1, 下降沿触发
 *   @arg       SYS_GPIO_RTIR, 2, 上升沿触发
 *   @arg       SYS_GPIO_BTIR, 3, 任意电平触发
 * @retval      无
 */
void sys_nvic_ex_config(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint8_t tmode)
{
    uint8_t offset;
    uint32_t gpio_num = 0;      /* gpio编号, 0~10, 代表GPIOA~GPIOG */
    uint32_t pinpos = 0, pos = 0, curpin = 0;

    gpio_num = ((uint32_t)p_gpiox - (uint32_t)GPIOA) / 0X400 ;/* 得到gpio编号 */
    RCC->APB2ENR |= 1 << 0;     /* AFIO = 1,使能AFIO时钟 */

    for (pinpos = 0; pinpos < 16; pinpos++)
    {
        pos = 1 << pinpos;      /* 一个个位检查 */
        curpin = pinx & pos;    /* 检查引脚是否要设置 */

        if (curpin == pos)      /* 需要设置 */
        {
            offset = (pinpos % 4) * 4;
            AFIO->EXTICR[pinpos / 4] &= ~(0x000F << offset);    /* 清除原来设置!!! */
            AFIO->EXTICR[pinpos / 4] |= gpio_num << offset;     /* EXTI.BITx映射到gpiox.bitx */

            EXTI->IMR |= 1 << pinpos;   /* 开启line BITx上的中断(如果要禁止中断,则反操作即可) */

            if (tmode & 0x01) EXTI->FTSR |= 1 << pinpos;        /* line bitx上事件下降沿触发 */

            if (tmode & 0x02) EXTI->RTSR |= 1 << pinpos;        /* line bitx上事件上升降沿触发 */
        }
    }
}

/**
 * @brief       GPIO重映射功能选择设置
 *   @note      这里仅支持对MAPR寄存器的配置, 不支持对MAPR2寄存器的配置!!!
 * @param       pos: 在AFIO_MAPR寄存器里面的起始位置, 0~24
 *   @arg       [0]    , SPI1_REMAP;         [1]    , I2C1_REMAP;         [2]    , USART1_REMAP;        [3]    , USART2_REMAP;
 *   @arg       [5:4]  , USART3_REMAP;       [7:6]  , TIM1_REMAP;         [9:8]  , TIM2_REMAP;          [11:10], TIM3_REMAP;
 *   @arg       [12]   , TIM4_REMAP;         [14:13], CAN_REMAP;          [15]   , PD01_REMAP;          [16]   , TIM15CH4_REMAP;
 *   @arg       [17]   , ADC1_ETRGINJ_REMAP; [18]   , ADC1_ETRGREG_REMAP; [19]   , ADC2_ETRGINJ_REMAP;  [20]   , ADC2_ETRGREG_REMAP;
 *   @arg       [26:24], SWJ_CFG;
 * @param       bit: 占用多少位, 1 ~ 3, 详见pos参数说明
 * @param       val: 要设置的复用功能, 0 ~ 4, 得根据pos位数决定, 详细的设置值, 参见: <<STM32中文参考手册 V10>> 8.4.2节, 对MAPR寄存器的说明
 *              如: sys_gpio_remap_set(24, 3, 2); 则是设置SWJ_CFG[2:0]    = 2, 选择关闭JTAG, 开启SWD.
 *                  sys_gpio_remap_set(10, 2, 2); 则是设置TIM3_REMAP[1:0] = 2, TIM3选择部分重映射, CH1->PB4, CH2->PB5, CH3->PB0, CH4->PB1
 * @retval      无
 */
void sys_gpio_remap_set(uint8_t pos, uint8_t bit, uint8_t val)
{
    uint32_t temp = 0;
    uint8_t i = 0;
    RCC->APB2ENR |= 1 << 0;     /* 开启辅助时钟 */

    for (i = 0; i < bit; i++)   /* 填充bit个1 */
    {
        temp <<= 1;
        temp += 1;
    }

    AFIO->MAPR &= ~(temp << pos);       /* 清除MAPR对应位置原来的设置 */
    AFIO->MAPR |= (uint32_t)val << pos; /* 设置MAPR对应位置的值 */
}

/**
 * @brief       GPIO通用设置
 * @param       p_gpiox: GPIOA~GPIOG, GPIO指针
 * @param       pinx: 0X0000~0XFFFF, 引脚位置, 每个位代表一个IO, 第0位代表Px0, 第1位代表Px1, 依次类推. 比如0X0101, 代表同时设置Px0和Px8.
 *   @arg       SYS_GPIO_PIN0~SYS_GPIO_PIN15, 1<<0 ~ 1<<15
 *
 * @param       mode: 0~3; 模式选择, 设置如下:
 *   @arg       SYS_GPIO_MODE_IN,  0, 输入模式(系统复位默认状态)
 *   @arg       SYS_GPIO_MODE_OUT, 1, 输出模式
 *   @arg       SYS_GPIO_MODE_AF,  2, 复用功能模式
 *   @arg       SYS_GPIO_MODE_AIN, 3, 模拟输入模式
 *
 * @param       otype: 0 / 1; 输出类型选择, 设置如下:
 *   @arg       SYS_GPIO_OTYPE_PP, 0, 推挽输出
 *   @arg       SYS_GPIO_OTYPE_OD, 1, 开漏输出
 *
 * @param       ospeed: 0~2; 输出速度, 设置如下(注意: 不能为0!!):
 *   @arg       SYS_GPIO_SPEED_LOW,  2, 低速
 *   @arg       SYS_GPIO_SPEED_MID,  1, 中速
 *   @arg       SYS_GPIO_SPEED_HIGH, 3, 高速
 *
 * @param       pupd: 0~3: 上下拉设置, 设置如下:
 *   @arg       SYS_GPIO_PUPD_NONE, 0, 不带上下拉
 *   @arg       SYS_GPIO_PUPD_PU,   1, 上拉
 *   @arg       SYS_GPIO_PUPD_PD,   2, 下拉
 *   @arg       SYS_GPIO_PUPD_RES,  3, 保留
 *
 * @note:       注意:
 *              1, 在输入模式(普通输入/模拟输入)下, otype 和 ospeed 参数无效!!
 *              2, 在输出模式下, pupd 参数无效!!(开漏输出无法使用内部上拉电阻!!)
 * @retval      无
 */
void sys_gpio_set(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint32_t mode, uint32_t otype, uint32_t ospeed, uint32_t pupd)
{
    uint32_t pinpos = 0, pos = 0, curpin = 0;
    uint32_t config = 0;        /* 用于保存某一个IO的设置(CNF[1:0] + MODE[1:0]),只用了其最低4位 */

    for (pinpos = 0; pinpos < 16; pinpos++)
    {
        pos = 1 << pinpos;      /* 一个个位检查 */
        curpin = pinx & pos;    /* 检查引脚是否要设置 */

        if (curpin == pos)      /* 需要设置 */
        {
            config = 0;         /* bit0~3都设置为0, 即CNF[1:0] = 0; MODE[1:0] = 0;  默认是模拟输入模式 */

            if ((mode == 0X01) || (mode == 0X02))   /* 如果是普通输出模式/复用功能模式 */
            {
                config = ospeed & 0X03;             /* 设置bit0/1 MODE[1:0] = 2/1/3 速度参数 */
                config |= (otype & 0X01) << 2;      /* 设置bit2   CNF[0]    = 0/1   推挽/开漏输出 */
                config |= (mode - 1) << 3;          /* 设置bit3   CNF[1]    = 0/1   普通/复用输出 */
            }
            else if (mode == 0)     /* 如果是普通输入模式 */
            {
                if (pupd == 0)   /* 不带上下拉,即浮空输入模式 */
                {
                    config = 1 << 2;               /* 设置bit2/3 CNF[1:0] = 01   浮空输入模式 */
                }
                else
                {
                    config = 1 << 3;                            /* 设置bit2/3 CNF[1:0] = 10   上下拉输入模式 */
                    p_gpiox->ODR &= ~(1 << pinpos);             /* 清除原来的设置 */
                    p_gpiox->ODR |= (pupd & 0X01) << pinpos;    /* 设置ODR = 0/1 下拉/上拉 */
                }
            }

            /* 根据IO口位置 设置CRL / CRH寄存器 */
            if (pinpos <= 7)
            {
                p_gpiox->CRL &= ~(0X0F << (pinpos * 4));        /* 清除原来的设置 */
                p_gpiox->CRL |= config << (pinpos * 4);         /* 设置CNFx[1:0] 和 MODEx[1:0], x = pinpos = 0~7 */
            }
            else
            {
                p_gpiox->CRH &= ~(0X0F << ((pinpos - 8) * 4));  /* 清除原来的设置 */
                p_gpiox->CRH |= config << ((pinpos - 8) * 4);   /* 设置CNFx[1:0] 和 MODEx[1:0], x = pinpos = 8~15 */

            }
        }
    }
}

/**
 * @brief       设置GPIO某个引脚的输出状态
 * @param       p_gpiox: GPIOA~GPIOG, GPIO指针
 * @param       0X0000~0XFFFF, 引脚位置, 每个位代表一个IO, 第0位代表Px0, 第1位代表Px1, 依次类推. 比如0X0101, 代表同时设置Px0和Px8.
 *   @arg       SYS_GPIO_PIN0~SYS_GPIO_PIN15, 1<<0 ~ 1<<15
 * @param       status: 0/1, 引脚状态(仅最低位有效), 设置如下:
 *   @arg       0, 输出低电平
 *   @arg       1, 输出高电平
 * @retval      无
 */
void sys_gpio_pin_set(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint8_t status)
{
    if (status & 0X01)
    {
        p_gpiox->BSRR |= pinx;  /* 设置GPIOx的pinx为1 */
    }
    else
    {
        p_gpiox->BSRR |= (uint32_t)pinx << 16;  /* 设置GPIOx的pinx为0 */
    }
}

/**
 * @brief       读取GPIO某个引脚的状态
 * @param       p_gpiox: GPIOA~GPIOG, GPIO指针
 * @param       0X0000~0XFFFF, 引脚位置, 每个位代表一个IO, 第0位代表Px0, 第1位代表Px1, 依次类推. 比如0X0101, 代表同时设置Px0和Px8.
 *   @arg       SYS_GPIO_PIN0~SYS_GPIO_PIN15, 1<<0 ~ 1<<15
 * @retval      返回引脚状态, 0, 低电平; 1, 高电平
 */
uint8_t sys_gpio_pin_get(GPIO_TypeDef *p_gpiox, uint16_t pinx)
{
    if (p_gpiox->IDR & pinx)
    {
        return 1;   /* pinx的状态为1 */
    }
    else
    {
        return 0;   /* pinx的状态为0 */
    }
}

/**
 * @brief       执行: WFI指令(执行完该指令进入低功耗状态, 等待中断唤醒)
 * @param       无
 * @retval      无
 */
void sys_wfi_set(void)
{
    __ASM volatile("wfi");
}

/**
 * @brief       关闭所有中断(但是不包括fault和NMI中断)
 * @param       无
 * @retval      无
 */
void sys_intx_disable(void)
{
    __ASM volatile("cpsid i");
}

/**
 * @brief       开启所有中断
 * @param       无
 * @retval      无
 */
void sys_intx_enable(void)
{
    __ASM volatile("cpsie i");
}

/**
 * @brief       设置栈顶地址
 * @note        左侧的红X, 属于MDK误报, 实际是没问题的
 * @param       addr: 栈顶地址
 * @retval      无
 */
void sys_msr_msp(uint32_t addr)
{
    __set_MSP(addr);    /* 设置栈顶地址 */
}

/**
 * @brief       进入待机模式
 * @param       无
 * @retval      无
 */
void sys_standby(void)
{
    RCC->APB1ENR |= 1 << 28;    /* 使能电源时钟 */
    PWR->CSR |= 1 << 8;         /* 设置WKUP用于唤醒 */
    PWR->CR |= 1 << 2;          /* 清除WKUP 标志 */
    PWR->CR |= 1 << 1;          /* PDDS = 1, 允许进入深度睡眠模式(PDDS) */
    SCB->SCR |= 1 << 2;         /* 使能SLEEPDEEP位 (SYS->CTRL) */
    sys_wfi_set();              /* 执行WFI指令, 进入待机模式 */
}

/**
 * @brief       系统软复位
 * @param       无
 * @retval      无
 */
void sys_soft_reset(void)
{
    SCB->AIRCR = 0X05FA0000 | (uint32_t)0x04;
}

/**
 * @brief       时钟设置函数
 * @param       plln: PLL倍频系数(PLL倍频), 取值范围: 2~16
 * @note
 *
 *              PLLCLK: PLL输出时钟
 *              PLLSRC: PLL输入时钟频率, 可以是 HSI/2, HSE/2, HSE等, 一般选择HSE.
 *              SYSCLK: 系统时钟, 可选来自 HSI/PLLCLK/HSE, 一般选择来自PLLCLK
 *              FCLK  : Cortex M3内核时钟, 等于HCLK
 *              HCLK  : AHB总线时钟, 来自 SYSCLK 的分频, 可以是1...512分频, 一般不分频
 *              PCLK2 : APB2总线时钟, 来自 HCLK 的分频(最大72Mhz), 可以是1/2/4/8/16分频, 一般不分频
 *              PCLK1 : APB1总线时钟, 来自 HCLK 的分频(最大36Mhz), 可以是1/2/4/8/16分频, 一般二分频
 *
 *              PLLCLK = PLLSRC * plln;
 *              FCLK = HCLK = SYSCLK;
 *              PCLK2 = HCLK;
 *              PCLK1 = HCLK / 2;
 *
 *              我们一般选择PLLSRC来自HSE, 即来自外部晶振.
 *              当外部晶振为 8M的时候, 推荐: plln = 9, AHB不分频, 得到:
 *              PLLCLK = 8 * 9 = 72Mhz
 *              FCLK = HCLK = SYSCLK = PLLCLK / 1 = 72Mhz
 *              PCLK2 = HCLK = 72Mhz
 *              PCLK1 = HCLK / 2 = 36Mhz
 *
 *              关于STM32F103的PLL说明详见: <<STM32中文参考手册 V10>>第六章相关内容
 *
 * @retval      错误代码: 0, 成功; 1, HSE错误;
 */
uint8_t sys_clock_set(uint32_t plln)
{
    // 我们不需要初始化时钟
    return 0;
}

/**
 * @brief       系统时钟初始化函数
 * @param       plln: PLL倍频系数(PLL倍频), 取值范围: 2~16
 * @retval      无
 */
void sys_stm32_clock_init(uint32_t plln)
{
    RCC->APB1RSTR = 0x00000000;     /* 复位结束 */
    RCC->APB2RSTR = 0x00000000;
    
    RCC->AHBENR = 0x00000014;       /* 睡眠模式闪存和SRAM时钟使能.其他关闭 */
    RCC->APB2ENR = 0x00000000;      /* 外设时钟关闭 */
    RCC->APB1ENR = 0x00000000;
    
    RCC->CR |= 0x00000001;          /* 使能内部高速时钟HSION */
    RCC->CFGR &= 0xF8FF0000;        /* 复位SW[1:0], SWS[1:0], HPRE[3:0], PPRE1[2:0], PPRE2[2:0], ADCPRE[1:0], MCO[2:0] */
    RCC->CR &= 0xFEF6FFFF;          /* 复位HSEON, CSSON, PLLON */
    RCC->CR &= 0xFFFBFFFF;          /* 复位HSEBYP */
    RCC->CFGR &= 0xFF80FFFF;        /* 复位PLLSRC, PLLXTPRE, PLLMUL[3:0] 和 USBPRE/OTGFSPRE */
    RCC->CIR = 0x009F0000;          /* 关闭所有RCC中断并清除中断标志 */

    sys_clock_set(plln);            /* 设置时钟 */

    /* 配置中断向量偏移 */
#ifdef  VECT_TAB_RAM
    sys_nvic_set_vector_table(SRAM_BASE, 0x0);
#else
    sys_nvic_set_vector_table(FLASH_BASE, 0x0);
#endif
}
// Inc/sys.h

/**
 ****************************************************************************************************
 * @file        sys.h
 * @author      正点原子团队(ALIENTEK)
 * @version     V1.1
 * @date        2020-04-17
 * @brief       系统初始化代码(包括时钟配置/中断管理/GPIO设置等)
 * @license     Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
 ****************************************************************************************************
 * @attention
 *
 * 实验平台:正点原子 STM32F103开发板
 * 在线视频:www.yuanzige.com
 * 技术论坛:www.openedv.com
 * 公司网址:www.alientek.com
 * 购买地址:openedv.taobao.com
 *
 * 修改说明
 * V1.0 20200417
 * 第一次发布
 *
 * V1.1 20221031
 * 在sys_stm32_clock_init函数添加相关复位/置位代码,关闭非必要外设,避免部分例程异常
 *
 ****************************************************************************************************
 */

#ifndef __SYS_H
#define __SYS_H

#include "stm32f1xx.h"


/**
 * SYS_SUPPORT_OS用于定义系统文件夹是否支持OS
 * 0,不支持OS
 * 1,支持OS
 */
#define SYS_SUPPORT_OS          0


/* sys_nvic_ex_config专用宏定义 */
#define SYS_GPIO_FTIR           1       /* 下降沿触发 */
#define SYS_GPIO_RTIR           2       /* 上升沿触发 */
#define SYS_GPIO_BTIR           3       /* 任意边沿触发 */

/* GPIO设置专用宏定义 */
#define SYS_GPIO_MODE_IN        0       /* 普通输入模式 */
#define SYS_GPIO_MODE_OUT       1       /* 普通输出模式 */
#define SYS_GPIO_MODE_AF        2       /* AF功能模式 */
#define SYS_GPIO_MODE_AIN       3       /* 模拟输入模式 */

#define SYS_GPIO_SPEED_LOW      2       /* GPIO速度(低速,2M) */
#define SYS_GPIO_SPEED_MID      1       /* GPIO速度(中速,10M) */
#define SYS_GPIO_SPEED_HIGH     3       /* GPIO速度(高速,50M) */

#define SYS_GPIO_PUPD_NONE      0       /* 不带上下拉 */
#define SYS_GPIO_PUPD_PU        1       /* 上拉 */
#define SYS_GPIO_PUPD_PD        2       /* 下拉 */

#define SYS_GPIO_OTYPE_PP       0       /* 推挽输出 */
#define SYS_GPIO_OTYPE_OD       1       /* 开漏输出 */

/* GPIO引脚位置宏定义  */
#define SYS_GPIO_PIN0           1<<0
#define SYS_GPIO_PIN1           1<<1
#define SYS_GPIO_PIN2           1<<2
#define SYS_GPIO_PIN3           1<<3
#define SYS_GPIO_PIN4           1<<4
#define SYS_GPIO_PIN5           1<<5
#define SYS_GPIO_PIN6           1<<6
#define SYS_GPIO_PIN7           1<<7
#define SYS_GPIO_PIN8           1<<8
#define SYS_GPIO_PIN9           1<<9
#define SYS_GPIO_PIN10          1<<10
#define SYS_GPIO_PIN11          1<<11
#define SYS_GPIO_PIN12          1<<12
#define SYS_GPIO_PIN13          1<<13
#define SYS_GPIO_PIN14          1<<14
#define SYS_GPIO_PIN15          1<<15


/*函数申明*******************************************************************************************/
/* 静态函数(仅在sys.c里面用到) */
static void sys_nvic_priority_group_config(uint8_t group);                      /* 设置NVIC分组 */


/* 普通函数 */
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset);             /* 设置中断偏移量 */
void sys_nvic_init(uint8_t pprio, uint8_t sprio, uint8_t ch, uint8_t group);    /* 设置NVIC */
void sys_nvic_ex_config(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint8_t tmode);   /* 外部中断配置函数,只针对GPIOA~GPIOK */
void sys_gpio_remap_set(uint8_t pos, uint8_t bit, uint8_t val);                 /* GPIO REMAP 设置 */
void sys_gpio_set(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint32_t mode, 
                  uint32_t otype, uint32_t ospeed, uint32_t pupd);              /*  GPIO通用设置 */
void sys_gpio_pin_set(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint8_t status);    /* 设置GPIO某个引脚的输出状态 */
uint8_t sys_gpio_pin_get(GPIO_TypeDef *p_gpiox, uint16_t pinx);                 /* 读取GPIO某个引脚的状态 */
void sys_standby(void);                     /* 进入待机模式 */
void sys_soft_reset(void);                  /* 系统软复位 */
uint8_t sys_clock_set(uint32_t plln);       /* 时钟设置函数 */
void sys_stm32_clock_init(uint32_t plln);   /* 系统时钟初始化函数 */


/* 以下为汇编函数 */
void sys_wfi_set(void);             /* 执行WFI指令 */
void sys_intx_disable(void);        /* 关闭所有中断 */
void sys_intx_enable(void);         /* 开启所有中断 */
void sys_msr_msp(uint32_t addr);    /* 设置栈顶地址 */

#endif

添加后,整个项目文件组织如下图所示:

在这里插入图片描述

GPIO口的初始化主要参考正点原子(见本节开头)。具体而言,首先使能GPIO口时钟,然后设置管脚状态。这里我们将向GPIOA0管脚输出高低电平以完成实验,则,初始化代码如下:

#include "sys.h"

...
    
int main(void) {
	...
    RCC->APB2ENR |= 1 << (0 + 2); // 使能PORTA时钟
	sys_gpio_set(GPIOA, 1 << 0, SYS_GPIO_MODE_OUT, SYS_GPIO_OTYPE_PP, SYS_GPIO_SPEED_MID, SYS_GPIO_PUPD_PU); // 设置0号管脚为输出,上拉状态    
}

5.3 产生并观察方波

接下来,为了向GPIOA口输出方波,一个很简单的思路就是每隔一段时间变换GPIOA口的电平状态。通过正点原子的例程可以发现,GPIO口的ODR寄存器可以起到这一作用。于是,可以简单编写代码如下:

...
    
void count_delay(int count)
{
    for (; count; count--)
        ;
}

void gen_square() {
    int i = 0;
    for (i = 0; i < 10000; i++) {
     	GPIOA->ODR ^= (1 << 0);	// 反复变化高低电平
        count_delay(1000); 
    }
}

int main(void) {
    ...
    RCC->APB2ENR |= 1 << (0 + 2); // 使能PORTA时钟
	sys_gpio_set(GPIOA, 1 << 0, SYS_GPIO_MODE_OUT, SYS_GPIO_OTYPE_PP, SYS_GPIO_SPEED_MID, SYS_GPIO_PUPD_PU); // 设置0号管脚为输出,上拉状态    
    gen_square()
}
...

接下来,Keil提供了一个很好地方法让我们观察GPIOA口的输出,具体而言,运行Debug,点击Logic Analyzersetup,点击新建Signal,输入PORTA.0(他会自动显示为PORTA & 0x00000001),这就代表我们要观察GPIOA口的0号管脚。此后,将Display Type修改为Bit以观察电平变化,然后点击Close。

在这里插入图片描述

按下F5全速运行,结果如下:

在这里插入图片描述

注意,由于我们在gen_wav函数中就跑了10000个循环,因此PORTA的0号管脚波形会戛然而止。类似地,可以实现输出正弦波等操作,这里给点思路就不给代码了:使用math.h提供的sin()函数可以很好地帮助实时计算位点。此时,应当利用起来PORTA的全部16个管脚(16位),并直接给ODR赋值。在Logic Analyzer中查看Analog值而非Bit值即可。下面是一个示意图:
在这里插入图片描述

至此,恭喜你,完成了第二题。

6. Keil信号函数和中断

首先,题目要求用Keil信号函数触发中断,首先需要了解什么是信号函数。相关参考资料较少,我贴在下面了:

  • keil MDK的信号函数_研究是为了理解的博客-CSDN博客
  • (1条消息) keil4中debug信号函数的简单使用_keil4怎么debug_寒一的博客-CSDN博客

这些讲得有点云里雾里,不过基本能够Get一个大概。基本上来说,这个信号函数是专门用于Debug场景的,与操作系统中的信号函数(signal)是两个不同的概念,可以通过Keil的Command栏跑起来这些函数,从而模拟一些外部操作。

为了完成该题目,最基本的思路是在程序中首先使能中断,然后在signal函数中触发中断,程序能够捕获中断信号并执行中断处理函数。初始化中断过程同样参考正点原子例程的实验4 外部中断实验,不多BB,直接上流程。

6.1 中断初始化

目前我们尝试使用GPIOD口的中断功能,具体而言,首先使能GPIOD口时钟,设置管脚状态,配置中断触发条件以及设置中断优先级。原子哥把中断初始化的很多函数都封装好了,因此,中断初始化非常简单,如下:

int main(void) {
    ...
	RCC->APB2ENR |= 1 << (5 + 2); // 使能PORTF时钟
	sys_gpio_set(GPIOD, 1 << 0, SYS_GPIO_MODE_OUT, SYS_GPIO_OTYPE_PP, SYS_GPIO_SPEED_MID, SYS_GPIO_PUPD_PU); 
    sys_nvic_ex_config(GPIOD, 1 << 0, SYS_GPIO_RTIR); /* 配置为上升沿触发中断 */
    sys_nvic_init(0, 2, EXTI0_IRQn, 2);           	  /* 中断优先级设置 */
    ...
}

其中,EXTI0_IRQn对应中断编号。当中断来临时,会自动触发EXTI0_IRQHandler对应的函数。相关定义可以通过全局搜索(这里我使用VSCode查看代码,更加方便)EXTI0_IRQHandler获取:
在这里插入图片描述

接下来编写中断处理函数,直接写就好了:

void EXTI0_IRQHandler(void)
{
    EXTI->PR = 1 << 0; /* 清除GPIOD管脚0对应的中断标志位 */
    putchar('D');
    putchar('e');
    putchar('a');
    putchar('d');
    putchar('p');
    putchar('o');
    putchar('o');
    putchar('l');
    putchar('l');
    putchar('o');
    putchar('v');
    putchar('e');
    putchar('s');
    putchar('h');
    putchar('i');
    putchar('s');
    putchar('s');
    putchar('t');
    putchar('a');
    putchar('r');
    putchar('\n');
    putchar('\r');
}

由于博主对于STM32的中断机制(EXTI)不是太了解,以后有机会研究一下这么多Handler到底是如何分发的,还是说大家都必须一起执行。

6.2 信号函数编写

基本思路为在信号函数中反复调整GPIOF的电平,这样就能触发中断的发生。为了编写信号函数,在项目根目录创建一个新的signal.ini文件,文件内容为:

signal void test(void)
{
    uint32_t GPIOD;
    GPIOD = 0x40000000UL + 0x00010000UL + 0x00001400UL;
    while (1) {
        *((volatile uint32_t *)(GPIOD + 0x0CUL)) ^= (1 << 0);
        printf("%x\n", GPIOD);
        twatch(1000000);
    }
}

这里需要说明的是,信号函数不能include之类的,所以我们必须手动计算GPIOF->ODR的地址并操作他,其中,GPIOF + 0x0CUL就对应ODR的地址,反复异或1即可。

6.3 综合运行

打开Debug模式,点击菜单栏Debug中的Function Editor

在这里插入图片描述

选择刚刚创建的signal.ini

在这里插入图片描述

点击Compile,可以观察到信号函数已经被导入:

在这里插入图片描述

接下来我们添加GPIODLogic Analyzer中(即输入PORTD.0,观察GPIOD口的0号管脚),便于观察信号函数的行为以及中断行为:
在这里插入图片描述

接下来F5全速运行,在命令行输入test()以调用信号函数,结果如下:
在这里插入图片描述

可以看到,在一段时间后,PORTD成功输出高低电平,串口不断打印以实现中断处理函数。

至此,恭喜你,完成题目三。

7. 总结

总的来说,这个课程项目我认为题目本身是很不错的,对于理解系统底层具有较好的帮助。但是缺乏Keil相关指导书和踩坑问题,使得实验起来很痛苦。最坑的地方在于想要使用ARMCM7的模拟器,但是按照官网的串口操作完全没用。最后明白是缺少对应的Dialog DLL(比如STM32对应的DLL是DARMSTM.DLL),使得模拟不能正常进行。目前尚不清楚是否可以下载ARM Cortex-M系列相关的Dialog DLL。曾几何时一度想要直接上RT-thread OS,但是OS把什么都封装好了,做起来毫无成就感,而且对理解底层帮助甚少。

希望本文能够对试图使用Keil模拟器功能的开发人员、学生、教师有所帮助,避免一些不必要的时间浪费。更换STM32F103系列芯片后,我们先后从直接操作地址,到使用封装的头文件,再到提取正点原子例程中的关键代码,逐步理解了STM32的硬件驱动流程以及Keil仿真串口、波形分析器的使用方法。

OK,现在开始就可以起飞了🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫

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

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

相关文章

佩戴舒适的蓝牙耳机有哪些品牌?不伤耳的蓝牙耳机推荐

​真无线蓝牙耳机逐渐成为大家日常必不可少的数码产品&#xff0c;也随着耳机的发展&#xff0c;人们对蓝牙耳机的要求也越来越高&#xff0c;不仅音质要好&#xff0c;长时间佩戴也要舒适&#xff0c;更是能够应用于多种场景中使用&#xff0c;但挑选蓝牙耳机也是一门学问&…

kettle开发-Day38-超好用自定义数据处理组件

目录 前言&#xff1a; 一、半斤八两&#xff0c;都不太行 1、表输入&#xff0c;速度快&#xff0c;但不稳妥 2、稳的一批&#xff0c;但是慢的像蜗牛 二、各诉衷肠&#xff0c;合作共赢 1、表输入&#xff0c;高效数据插入 2、插入更新&#xff0c;一个都不能少 三、表输…

## flink- mysql同步数据至starrocks-2.5.0之数据同步

flink- mysql同步数据至starrocks-2.5.0之数据同步 mysql 创建 表 CREATE TABLE t_user (id bigint NOT NULL AUTO_INCREMENT,user_name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,age int DEFAULT NULL,PRIMARY KEY (id) ) ENGINEInnoDB…

手机app测试杂谈

手机上的 app 分为基于 HTML5 的 app(类似于 pc 上的 b/S 应用)和本地 app(类似于 C/S 结构)。 所以测试上我们也可以充分吸收 web 的 b/s 和 c/s 测试经验。但是不同于 pc 上的应用 测试&#xff0c;手机上的测试有其独特性 测试前的思考:我们这个产品主要是做什么的?为什么我…

03. 青龙面板配置B站快速升级任务天选时刻脚本(保姆级图文)

目录 功能介绍与环境要求1. 修改配置文件拉取.sh脚本2. 拉取库脚本3. 安装 dotnet 环境4.1 扫码登录方式4.2 b站cookie方式登录&#xff08;如果你扫码成功了就不用看这个了&#xff09;获取cookie新建cookie的环境变量 5. 配置任务设置变量6. 运行每日任务测试一下总结 欢迎关…

搭建cloud项目以及各个依赖和配置说明

文章目录 背景步骤配置父pom文件spring-cloud和spring-cloud-alibaba的区别 添加网关模块配置网关的application.yml文件网关入口 普通模块普通模块的配置文件&#xff1a;普通模块的pom文件启动类&#xff1a;application.yml文件和bootstrap.yml文件的区别 总结 背景 最近在…

chatgpt赋能python:Python描述符:更加灵活的属性管理方式

Python描述符&#xff1a;更加灵活的属性管理方式 Python是一种高级编程语言&#xff0c;它的简单易用、高效性和灵活性使得它成为了现代企业和开发者的首选开发语言之一。然而&#xff0c;在使用Python编写代码时&#xff0c;很多时候需要对属性进行访问和修改&#xff0c;而…

C++单目运算符和特殊运算符的重载(9)

运算符的重载 原理和机制 C中运算符只支持基本数据类型运算&#xff0c;如果需要运算符支持类类型的运算&#xff0c;需要使用C提供的新语法 ------- 运算符的重载 运算符的重载本质上是通过函数来实现的&#xff0c;将类类型数据的运算过程写成一个特殊的函数&#xff0c;当…

YOLOv8 图像分割

一、背景 二、环境配置 官网&#xff1a;Previous PyTorch Versions | PyTorch cuda 11.7 pytorch 1.13.0 torchvision 0.14.0 pytorch-cuda 11.7 三、安装yolov8 官网&#xff1a;GitHub - ultralytics/ultralytics: NEW - YOLOv8 &#x1f680; in PyTorch > ONNX &…

Mendix 10 树形组件分析及应用

一、前言 产品研发目标是服务于业务&#xff0c;解决具体业务问题&#xff0c;带来业务价值。 因此&#xff0c;任何产品功能的推出&#xff0c;都应该秉承“从实践中来&#xff0c;到实践中去”的原则&#xff0c;在实战中发现问题&#xff0c;通过新功能设计和功能改进解决…

chatgpt科普

引言 chatgpt没有国内开放&#xff0c;为什么如此重要。抛开技术细节&#xff0c;少用专业名词&#xff0c;在整体功能上讲解 ChatGPT 的工作原理、制造过程、涌现的能力、未来的影响以及如何应对。让大家明白&#xff1a; ChatGPT是如何回答问题的。 它是怎么被制造的&…

Go语言实现单链表

博主最近在学习Go语言&#xff0c;所以打算更新一期Go语言版本的数据结构。这篇文章将的是Go语言如何实现单链表。 文章目录 前言一、个人见解&#xff0c;为什么学GO&#xff1f;二、Go语言实现单链表1.创建节点2.通过数组创建一个单链表3.遍历单链表4.单链表插入操作4.1 伪代…

HOOPS Visualize SDK 2023 Crack

桌面和移动工程应用程序的图形引擎 HOOPS Visualize 是 3D 图形 SDK&#xff0c;支持来自市场领导者 Hexagon、Trimble、Ansys、SOLIDWORKS、™ Autodesk 等的数百个工程应用程序。 用于 3D CAD 渲染的图形 SDK HOOPS Visualize 是一个以工程为中心的高性能图形库&#xff0c…

安卓蓝牙ATT协议介绍

介绍 ATT&#xff0c;Attribute Protocol&#xff0c;用于发现、读、写对端设备的协议(针对BLE设备) ATT允许蓝牙远程设备&#xff08;比如遥控器&#xff09;作为服务端提供拥有关联值的属性集&#xff0c;让作为客户端的设备&#xff08;比如手机、电视&#xff09;来发现、…

通用能力及AI核心能力表现优异!合合信息智能文档处理系统(IDP)高评级通过中国信通院评估

数字经济快速发展的背后&#xff0c;全球数据总量呈现出爆发式增长趋势。智能文档处理&#xff08;IDP&#xff09;技术能够高效地从多格式文档中捕捉、提取和处理数据&#xff0c;帮助机构和企业大幅提升文档处理效率&#xff0c;节约时间和人力成本。近期&#xff0c;合合信息…

C语言进阶--字符函数与内存函数

目录 一.字符函数 1.strlen函数 模拟实现strlen 2.strcpy函数 模拟实现strcpy 3.strcat函数 模拟实现strcat strcat能否用于自己追加自己&#xff1f; 4.strcmp函数 模拟实现strcmp 5.strncpy函数 6.strncat函数 7.strncmp函数 模拟实现strncmp 8.strstr函数 模…

哨兵2号数据下载与利用Python处理(波段融合、降采样、可视化、裁剪等)

简单介绍 网址:https://scihub.copernicus.eu/dhus/#/home 哨兵2号(Sentinel-2)是欧洲空间局(European Space Agency,简称ESA)推出的一组遥感卫星,旨在为地球观测和环境监测提供高质量的光学图像数据。 S2MSI2A是哨兵2号卫星的一种传感器。 S2MSI2A是哨兵2号卫星搭载…

huggingface - PEFT.参数效率微调

GitHub - huggingface/peft: &#x1f917; PEFT: State-of-the-art Parameter-Efficient Fine-Tuning. 最先进的参数高效微调 (PEFT) 方法 Parameter-Efficient Fine-Tuning (PEFT) 方法可以使预训练语言模型 (PLM) 高效适应各种下游应用程序&#xff0c;而无需微调模型的所有…

记录--Vue3 封装 ECharts 通用组件

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 按需导入的配置文件 配置文件这里就不再赘述&#xff0c;内容都是一样的&#xff0c;主打一个随用随取&#xff0c;按需导入。 import * as echarts from "echarts/core"; // 引入用到的图表…

ctfshow web入门 php特性 web93-97

1.web93 intval($num,0),0代表根据变量类型进行使用哪一种进制进行取整 可以使用8进制&#xff0c;正负数&#xff0c;小数点 payload: 010574 4476.0 4476.0 2.web94 过滤了0&#xff0c;不能使用8进制了&#xff0c;还可以使用小数点&#xff0c;正负数等 payload&#xff1…