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

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

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

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

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

1. 项目介绍

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第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
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第2张图片

  • 随便保存在一个文件夹(博主创建了一个空的Demo文件夹)中后选择STM32F103CB(这里和STM32F103RE不一样,但问题不大)
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第3张图片

  • 弹出来的对话框是安装运行时库,其实就是提供一些模块化代码。参考创建keil仿真工程(ARM内核)_繁华一世终的博客-CSDN博客,勾选CMSIS中的COREDevice中的Startup
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第4张图片

  • 进入如下界面(点加号把Target 1展开就能看到),可以对各种文件夹重命名,这里博主把Source Group 1命名为Src:
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第5张图片

  • 右键添加新文件,我们要添加一个main.c
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第6张图片

  • 接下来,我们把这位大哥(创建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魔术棒
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第7张图片

  • 切换到Debug栏,做如下修改,点击OK关闭对话框(注意,如果选的STM32F103xx,那后面的Parameter就一定要填STM32F103xx
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第8张图片

  • 点击Debug按钮,进入Debug模式
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第9张图片

  • flag变量加入逻辑分析器中,按下F5,全速运行
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第10张图片

  • 点击Logic Analyzer中的auto以自动缩放比例
    HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第11张图片

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

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.
HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第12张图片

F5全速运行,结果如下图所示,可以看到,下方UART #1串口窗口出现了Deadpoolloveshisstar
HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第13张图片

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

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,可能你还得自己找找,代码贴不出来,太多了……)。

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第14张图片

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

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第15张图片

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

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第16张图片

可以看到在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处添加对应设备即可:
HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第17张图片

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位数决定, 详细的设置值, 参见: <> 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说明详见: <>第六章相关内容
 *
 * @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

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

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第18张图片

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。

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第19张图片

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

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第20张图片

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

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

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获取:
HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第22张图片

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

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

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

选择刚刚创建的signal.ini

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第24张图片

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

HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第25张图片

接下来我们添加GPIODLogic Analyzer中(即输入PORTD.0,观察GPIOD口的0号管脚),便于观察信号函数的行为以及中断行为:
HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第26张图片

接下来F5全速运行,在命令行输入test()以调用信号函数,结果如下:
HITSZ嵌入式计算(研)23年Keil模拟器项目解决方案_第27张图片

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

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

7. 总结

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

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

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

你可能感兴趣的:(操作系统,项目集锦,单片机,嵌入式硬件,Keil模拟器)