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模拟器实现几个嵌入式系统中的基本功能,但麻烦点在于:
- Keil模拟器资料较少,国内外的文档不够,社区不够活跃,许多问题找不到解决方案,非常多的坑,本文帮助大家避免坑;
- 本课程不会介绍任何与Keil相关、嵌入式系统底层开发相关的问题,所有实现都需要自行搜索,从而加剧问题1;
- Keil UI界面太丑,开发起来很恶心,还好不用做大系统开发……
博主已将代码放在github以供参考,不多BB,直接开始讲解
1. 项目介绍
简单来说:
- 安装Keil
- 创建一个新项目
- 从Keil的模拟器启动
- 编写程序
- 向串口发送数据
- 输出周期波
- 使用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布局(即0x16000000
为UART0
的起始地址)根本无法在模拟器上访问。因此,在挣扎半天后放弃使用ARMCM7
,而使用STM32F103RE
芯片(STM32F103CB
应该也可以,这里使用STM32F103RE
主要想用STM32F103xE.h
头文件中定义的DAC模电转换模块,然而发现模拟器上似乎并不能使用)。
下面正式介绍具体创建流程:
-
打开Keil uV5,点击工具栏的Project,下拉选择new uVision Project
-
随便保存在一个文件夹(博主创建了一个空的Demo文件夹)中后选择
STM32F103CB
(这里和STM32F103RE
不一样,但问题不大)
-
弹出来的对话框是安装运行时库,其实就是提供一些模块化代码。参考创建keil仿真工程(ARM内核)_繁华一世终的博客-CSDN博客,勾选
CMSIS
中的CORE
和Device
中的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 DLL和Parameter一定要改对;
网上有许多介绍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.c
和Inc/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.c
与sys.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口时钟,然后设置管脚状态。这里我们将向GPIOA
口0管脚
输出高低电平以完成实验,则,初始化代码如下:
#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 Analyzer
的setup
,点击新建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
,可以观察到信号函数已经被导入:
接下来我们添加GPIOD
到Logic 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,现在开始就可以起飞了🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫