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

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. 总结

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;elseflag = 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 5usart1->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_Hvoid uart_init(void);
int getchar(void);
int putchar(char c);#endif

修改main.c如下:

// Src/main.c#include "uart.h"...int main(void)
{uart_init(); // 初始化串口// putcharputchar('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_RAMsys_nvic_set_vector_table(SRAM_BASE, 0x0);
#elsesys_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,现在开始就可以起飞了🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫

本文链接:https://my.lmcjl.com/post/1354.html

展开阅读全文

4 评论

留下您的评论.