H_On个人小站

个人站

这都被你发现了【惊
奖励你一朵小红花~


从零开始的 STM32

目录

前言

由于手边缺少传感器,电阻,LED 等配件,导致 Pi Pico 的学习进度中断。因此直接开始尝试重新拾起 STM32。

本次使用的是 STM32 最小系统板,芯片型号为 STM32F103C8Tb 991KA 93 MYS 801

软件准备

参考 参考文章 进行一个 Keil 的安装和配置,在做下面一系列步骤遇到问题时可以看看那几篇:

  1. 进入官网下载 Keil
  2. 然后在网上冲浪找到能用的注册机
  3. 进入 keil 中,点击 “pack installer” 组件安装 STM32F10 包。注意右下角有个进度条,点击 install 后要等它变成 “Up to data” 才表示安装成功
  4. 新建工程,选择对应的芯片,教程中选的是 STM32F103RC 我选的是 STM32F103C8
  5. 之后勾选需要的库,按照下图所示勾选,后面也可以继续添加,这里给一个 参考连接
  6. 打开 Target 1 右键 Source Group 1 添加新文件 Add New Item 选择第一项 C File 命名为 .c 文件即可,如 main.c
    • 如果编辑器中无法显示中文,Edit->Configuration->Editor->Encoding: UTF-8

经典点灯

首先熟悉一下如何往 STM32 中烧录程序:

  1. 往工程中加入 or 新建一个 main.c 文件并写入下面的代码,这个代码是调用了最小系统板上的板载 LED 来进行引脚 PC13 的 IO 测试
  2. 点击魔法棒->Output->勾选 Creat HEX File,想看操作截图可以到 这里
  3. 依次点击编译按钮,查看代码是否有问题,只要没有 Error 就没关系,点击中间的构建(Build)按钮(或 F7)就会在工程文件同目录的 Objects 生成一个与工程同名的 .hex 文件
  4. 将 STM32 通过 TTL 转 USB 连接到电脑上,工具和接线的图示看 这里
  5. 打开 mcuisp 下载程序,按照 这里 操作,可能会遇到的问题这里也有
  6. 最后你可以尝试修改代码,如闪烁中的演示长度,并再次编译(F7)之后下载到 STM32 上
    1. 这时如果 mcuisp 一直等待连接,你需要按一下板子上的 RESET 按钮重新上电
    2. 也可以拔掉 USB 转 TTL 再插上电脑,然后再次点击 “开始编程” 按钮
  7. 如果你想让下载好的程序保持运行,拔掉之后,把跳线帽还原,再用任意方式上电即可
#include "stm32f10x.h"

void delay(uint32_t n) {
	uint32_t t;
	SysTick->CTRL = 0x00;
	SysTick->LOAD = n*(uint16_t)(SystemCoreClock/8000000)*1000;
	SysTick->VAL = 0x00;
	SysTick->CTRL |= 0x01;
	do {t = SysTick->CTRL;} while ((t&0x01)&&!(t&(1<<16)));
	SysTick->CTRL = 0x00;
	SysTick->VAL = 0x00;
}

void led_init(void) {
	GPIO_InitTypeDef GPIO_InitStruct;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_13;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &GPIO_InitStruct);
	GPIO_SetBits(GPIOC, GPIO_Pin_13);
}

int main(void) {
	led_init();
	while (1) {
		GPIO_ResetBits(GPIOC, GPIO_Pin_13);
		delay(500);
		GPIO_SetBits(GPIOC, GPIO_Pin_13);
		delay(500);
	}
}

代码分析

头文件

#include "stm32f10x.h"
// #include "stm32f10x_rcc.h"
// #include "stm32f10x_gpio.h"

理论上来说接口结构体 GPIO_InitTypeDef 定义在函数库 stm32f10x_gpio.h 中,GPIO 使能函数 RCC_APB2PeriphClockCmd 定义在函数库 stm32f10x_rcc.h 中。然而实际测试中上面三个库声明其中任意一个程序都可以正常编译,无非是 Warning 的个数会加减 2 个。

学徒我不求甚解,可能跟 keil5 这个编译软件的工作原理有关,暂时先不纠结。

引脚初始化

void led_init(void) {
	// 定义 GPIO 结构体,下面初始化接口函数使用
	GPIO_InitTypeDef GPIO_InitStruct;
	// 初始化引脚工作模式为 通用推挽输出
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	// 使用 PC13 引脚
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
	// 初始化引脚速度
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

	// 使能 GPIOC
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	
	// 初始化指定引脚
	GPIO_Init(GPIOC, &GPIO_InitStruct);
	// 使 GPIO 输出高电平,ResetBits 是输出低电平
	GPIO_SetBits(GPIOC, GPIO_Pin_13);
}

计时器初始化

为了实现精准的延时,我们需要了解单片机的工作频率并加以运用。幸运的是 STM32 使用的 Cortex-M3 架构处理器自带一个简易的滴答计时器(SysTick) 让我们能够实现一个便于理解的计时器。

static uint8_t  D_us = 0;
static uint16_t D_ms = 0;

void delay_init(void) {
	// 设置系统 SysTick 时钟为系统时钟的 8 分频
	// 系统时钟为 72MHZ 所以 SysTick 的时钟为 9MHZ
	// 也就是说 SysTick->value 中的值每减少 1 需要花费 1/9 微秒的时间
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
	
	// 获取微秒和毫秒的值
	// 微妙系数 = 系统时钟(72MHZ)/8M = 9
	D_us = SystemCoreClock/8000000;
	// 毫秒系数为微秒的 1000 倍
	D_ms = (uint16_t)D_us*1000;
}
  • D_us D_ms 这两个系数是定义在头文件下面的全局变量,为了在初始化中修改值,并在下面的 delay 函数中使用
  • STM32 可以使用多种系统时钟,这个频率也可以自己定义,但是对于初学者来说,只要知道默认的频率是 72MHZ 即可
  • 微妙系数 D_us 可以理解为 “度过 1 微妙需要芯片运行的次数/系统震动的次数”
    • 系统频率 72MHZ8 分频后是 9MHZ,即 1s 震动 9M 次,1us 震动 9
  • 同理,1ms = 1000us,因此 1ms 震动 9000

延时函数

// 微秒延时
void delay_us(uint32_t n) {
	uint32_t t;
	// 关闭 SysTick 定时器
	SysTick->CTRL = 0x00;
	// 修改重装载寄存器中的值
	// SysTick->LOAD = n*(uint16_t)(SystemCoreClock/8000000)*1000;
	SysTick->LOAD = n*D_us;
	// 清空计数器
	// 计数器 VAL 为 0 时会重载 LOAD 的值,同时 CTRL 寄存器中清除 COUNTFLAG 的标志
	SysTick->VAL = 0x00;
	// 打开定时器
	SysTick->CTRL |= 0x01;
	// SysTick->CTRL 第  0 位表示 ENABLE 定位器的使能位
	// SysTick->CTRL 第 16 位表示 COUNTFLAG
	// 上次读 CTRL 寄存器时 VAL 若为 0,则该位为 1
	// 初始化时 VAL 设为 0 后,SysTick->CTRL |= 0x01; 这条代码读取了一下 CTRL 寄存器,此时 16 位为 1,且 VAL 已经重载
	// 但接下来的 do while 循环会无条件的先执行一次 t = SysTick->CTRL; 再次读取 CTRL 此时 VAL 不为 0,因此需要等待倒数结束才能得到 t&(1<<16) = 1 从而退出 while 循环
	do { t = SysTick->CTRL; } while ((t&0x01) && !(t&(1<<16)));
	// 关闭
	SysTick->CTRL = 0x00;
	// 清空
	SysTick->VAL = 0x00;
}
  • 配置定时器之前先关闭定时器 SysTick->CTRL = 0x00; 避免数据错乱
  • SysTick->LOAD 为计数数量,1us 需要震动 D_us 次,nus 需要震动 n*D_us
  • 打开定时器 SysTick->CTRL |= 0x01; 后,定时器就会开始对系统震动进行计数
  • do while 循环不断检测打开定时器后震动次数是否达到 n*D_us,次数到达后结束循环

毫秒级延时同理

void delay_ms(uint32_t n) {
	uint32_t t;
	SysTick->CTRL = 0x00;
	SysTick->LOAD = n*D_ms;
	SysTick->VAL = 0x00;
	SysTick->CTRL |= 0x01;
	do { t = SysTick->CTRL; } while ((t&0x01) && !(t&(1<<16)));
	SysTick->CTRL = 0x00;
	SysTick->VAL = 0x00;
}

甚至可以这样

void delay(uint8_t type,  uint32_t n) {
	uint32_t t;
	SysTick->CTRL = 0x00;
	SysTick->LOAD = n*(type?D_ms:D_us);
	SysTick->VAL = 0x00;
	SysTick->CTRL |= 0x01;
	do { t = SysTick->CTRL; } while ((t&0x01)&&!(t&(1<<16)));
	SysTick->CTRL = 0x00;
	SysTick->VAL = 0x00;
}

点灯

int main(void) {
	delay_init();
	led_init();
	while (1) {
		// 输出低电平开灯
		GPIO_ResetBits(GPIOC, GPIO_Pin_13);
		delay(1, 100000);
		// 输出高电平关灯
		GPIO_SetBits(GPIOC, GPIO_Pin_13);
		delay(1, 100000);
	}
}
  • 进入 main() 函数首先初始化我们需要用到的模块
  • 尝试把 1 改成 0 查看代码运行结果

末尾闲言

曾经拿 32 做了一个小电子琴当课设,现在重新学习 32 想复刻却没找到任何资料,只找到一篇自己写的电灯博客。环境搭建倒是记录的挺详细的,里面粘贴的代码却完全没有解释。

本篇入门就先以把初学接触到的代码完全理解为目标进行学习,成果如上,希望能对各位以及未来的我有所帮助。

参考文章

参考视频