5 模范工程
我们建立一个模范工程,把常用的功能整合到一起,以后只用将模范工程粘贴复制就能快速创建一个拥有很多自己写的库的工程文件了。
5.1 delay
总所周知,HAL_Delay()
只能实现ms级别的延时,但如果我们想实现us级别的延时,则HAL库就没有相应的库函数了。于是,得我们自己编写。
这里笔者创建了新的路径,根目录下创建System文件夹(用于放笔者觉得经常调用的函数),里面创建delay文件夹,并在其中新建delay.c/.h
那么如何实现延时呢?
笔者相信玩过51的朋友不会没有想法,最简单的两种想法就是使用执行语句时产生的延时,或者使用硬件定时器实现精确定时。这次我们要讲的就是基于定时器的精确定时。
SysTick定时器,我们在之前配置时系统时钟时就见过它一面。这是一个很基础的定时器,它有三个常用寄存器,分别是 CTRL、LOAD、VAL。当然,我们可以用上章讲的结构体指针去实现指向地址偏移而访问这个寄存器。
SysTick->CTRL
SysTick-> LOAD
SysTick-> VAL
但由于我们之前配置系统时钟使用的是SysTick定时器,所以我们还有一个要求就是不能修改SysTick值的情况下,实现计数。
delay.h
//
// Created by Whisky on 2023/1/8.
//
#ifndef HELLOWORLD_DELAY_H
#define HELLOWORLD_DELAY_H
#include "main.h"
void delay_init(uint16_t sysclk); //单位为MHZ
void delay_ms(uint16_t nms);
void delay_us(uint32_t nus);
#endif //HELLOWORLD_DELAY_H
delay.c里
static uint32_t fac_us=0;
void delay_init(uint16_t sysclk)
{
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
fac_us = sysclk;
}
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
这一句把 SysTick 的时钟选择外部时钟,这里需要注意的是:SysTick 的时钟源自 HCLK,假设我们外部晶振为 8M,然后倍频到 72M,那么 SysTick 的时钟即为 72Mhz,也就是 SysTick 的计数器 VAL 每减 1,就代表时间过了 1/72us。
所以 fac_us=SYSCLK;
这句话就是计算在 SYSCLK 时钟频率下延时 1us需要多少个 SysTick 时钟周期。
fac_us,为 us 延时的基数,也就是延时 1us,Systick 定时器需要走过的时钟周期数。
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told,tnow,tcnt=0;
uint32_t reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}
这里就正是利用了我们前面提到的时钟摘取法,ticks 是延时 nus 需要等待的 SysTick 计数次数(也就是延时时间),told 用于记录最近一次的 SysTick->VAL 值,然后 tnow 则是当前的SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存放在 tcnt 里面,然后通过对比 tcnt 和 ticks,来判断延时是否到达,从而达到不修改 SysTick 实现 nus 的延时。
这样实现delay_ms
的函数就非常容易了
void delay_ms(uint16_t nms)
{
uint32_t i;
for(i=0;i<nms;i++) delay_us(1000);
}
附上整个delay.c
//
// Created by Whisky on 2023/1/8.
//
#include "delay.h"
//单位为MHZ
static uint32_t fac_us=0;
void delay_init(uint16_t sysclk)
{
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
fac_us = sysclk;
}
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told,tnow,tcnt=0;
uint32_t reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}
void delay_ms(uint16_t nms)
{
uint32_t i;
for(i=0;i<nms;i++) delay_us(1000);
}
在main函数的初始化代码中添加对头文件的引用并调用初始化函数:
#include "delay"
delay_init(72); //因为笔者之前设置的频率为72MHZ
5.2 常用的缩写
uint8_t 实在太长,相信你经常能看见有u8这样的简写
我们也整一个
创建System/sys.h
sys.h
//
// Created by Whisky on 2023/1/9.
//
#ifndef HELLOWORLD_SYS_H
#define HELLOWORLD_SYS_H
#include "main.h"
typedef uint64_t u64;
typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t u8;
#endif //HELLOWORLD_SYS_H
5.3 神奇的位带操作
假如有学习过51的朋友就会感觉STM32的代码真是又臭又长,想我51大法只用0、1赋值便能控制寄存器相位原子位(相应bit位),而这STM32还要Read来Write去,填这么多参数。
这里,笔者就要告诉大家,其实是有方法的——位带操作。
先来看一下Cortex-M3权威指南中描述的位带操作:
支持了位带操作后,可以使用普通的加载/存储指令来对单一的比特进行读写操作。在CM3中,有两个区中实现了位带。其中一个是 SRAM 区的最低1MB 范围,第二个则是片内外设区的最低 1MB 范围。这两个区中的地址除了可以像普通的 RAM 一样使用外,它们还都有自己的“位带别名区”,位带别名区把每个比特膨胀成一个 32 位的字。当你通过位带别名区访问这些字时,就可以达到访问原始比特的目的。
位带操作简单的说,就是把每个比特膨胀为一个 32 位的字,当访问这些字的时候就达到了访问比特的目的,比如说 BSRR 寄存器有 32 个位,那么可以映射到 32 个地址上,我们去访问这 32 个地址就达到访问 32 个比特的目的。这样我们往某个地址写 1 就达到往对应比特位写 1 的目的,同样往某个地址写 0 就达到往对应的比特位写 0 的目的。
在STM32中有两个区域可以进行位带操作,支持位带操作的两个内存区的范围是:
0x2000 0000-0x200F FFFF (SRAM区中的最低1MB)
0x4000 0000-0x400F FFFF (片上外设区中的最低1MB)
对应别名区的范围为:
0x2200 0000-0x23FF FFFF
0x4200 0000-0x43FF FFFF
下面就是SRAM的位带图:
位带区的一位,在别名区是32位。
转换方式
例如操作GPIOB5->ODR寄存器(GPIOB_ODR寄存器的地址为0x4001080c,则A=0x4001080c)
位带区: 支持位带操作的地址区。
位带别名: 对别名地址的访问最终会变换成对位带区的访问。
AliasAddr= 0x42000000 + ( (A - 0x40000000) * 8 + n) * 4 =0x42000000+ (A - 0x40000000) * 32 + 4*n
所操作的位带别名区地址:
AliasAddr =*(volatile uint32_t)0x42000000+((0x4001080c-0x40000000)*8+5)*4
=*(volatile uint32_t)0x42000000+ (0x4001080c-0x40000000)*32 + 5*4
=*(volatile uint32_t)0x42218194
这样我们就了解是如何进行转化的了,但是这就有新的问题了,32位的位带地址是如何给的位带区传递值的呐。
官方文档给出了解释:
在位带区中,每个比特都映射到别名地址区的一个字,这是只有一个 LSB有效的字。当一个别名地址被访问时,会先把该地址变换成位带地址。对于读操作,读取位带地址中的一个字,再把需要的位右移到LSB,并把LSB返回。对于写操作,把需要写的位左移至对应的位序号处,然后执行一个原子的“读一改一写”过程。
注释:LSB–最低有效位
这样位带操作就显得很简单了:
- 将位带区地址的计算宏定义成“地址+偏移”
- 再将该地址转换成一个指向该位带别名区的指针
- 宏定义访问位带别名区地址
于是完善sys.h
//
// Created by Whisky on 2023/1/9.
//
#ifndef HELLOWORLD_SYS_H
#define HELLOWORLD_SYS_H
#include "main.h"
typedef uint64_t u64;
typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t u8;
//位带操作,实现51类似的GPIO控制功能
//具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).
//IO口操作宏定义
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
#define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出
#define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入
#define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出
#define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
#endif //HELLOWORLD_SYS_H
这样out为输出,in为输入
于是,我们开灯甚至可以写成:
PBout(5) = 0;//GPIO已经提前配置好了
闪光灯
PBout(5) = !PBout(5);
delay_ms(500);
非常的优雅。
主要参考:
STM32位带操作-详解-计算过程
【32单片机学习】(1)stm32位带操作
正点原子STM32F103官方例程
5.4 usart
串口相关知识,笔者将在之后讲解串口的时候给大家详细讲解。本节我们只给大家讲解比较独立的 printf 函数支持相关的知识。
这里使用重定向方式,实现对printf的改写
源码来源:
配置CLion用于STM32开发【优雅の嵌入式开发】
创建System/usart文件夹,在里面添加
retarget.h
#ifndef HELLOWORLD_RETARGET_H
#define HELLOWORLD_RETARGET_H
#include "stm32f1xx_hal.h"
#include <sys/stat.h>
#include <stdio.h>
void RetargetInit(UART_HandleTypeDef *huart);
int _isatty(int fd);
int _write(int fd, char *ptr, int len);
int _close(int fd);
int _lseek(int fd, int ptr, int dir);
int _read(int fd, char *ptr, int len);
int _fstat(int fd, struct stat *st);
#endif //HELLOWORLD_RETARGET_H
retarget.c
#include <_ansi.h>
#include <_syslist.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/times.h>
#include <retarget.h>
#include <stdint.h>
#if !defined(OS_USE_SEMIHOSTING)
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
UART_HandleTypeDef *gHuart;
void RetargetInit(UART_HandleTypeDef *huart)
{
gHuart = huart;
/* Disable I/O buffering for STDOUT stream, so that
* chars are sent out as soon as they are printed. */
setvbuf(stdout, NULL, _IONBF, 0);
}
int _isatty(int fd)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 1;
errno = EBADF;
return 0;
}
int _write(int fd, char *ptr, int len)
{
HAL_StatusTypeDef hstatus;
if (fd == STDOUT_FILENO || fd == STDERR_FILENO)
{
hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return len;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _close(int fd)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 0;
errno = EBADF;
return -1;
}
int _lseek(int fd, int ptr, int dir)
{
(void) fd;
(void) ptr;
(void) dir;
errno = EBADF;
return -1;
}
int _read(int fd, char *ptr, int len)
{
HAL_StatusTypeDef hstatus;
if (fd == STDIN_FILENO)
{
hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return 1;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _fstat(int fd, struct stat *st)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
{
st->st_mode = S_IFCHR;
return 0;
}
errno = EBADF;
return 0;
}
#endif //#if !defined(OS_USE_SEMIHOSTING)
添加这两个文件到工程,编译之后会发现,有几个系统函数重复定义了,被重复定义的函数位于Src目录的syscalls.c文件中,我们把里面重复的几个函数删掉即可。
在main函数的初始化代码中添加对头文件的引用并注册重定向的串口号:
#include "retarget.h"
RetargetInit(&huart1);
然后就可以愉快地使用printf
和scanf
啦:
char buf[100];
printf("\r\nYour name: ");
scanf("%s", buf);
printf("\r\nHello, %s!\r\n", buf);
当然,如果你不知道huart如何来的,不用慌张,等到笔者讲解到串口时,便会告诉大家。
5.5 bsp.h
由于发现我们一下子要引用很多的头文件,而且这些头文件可能相互引用。
于是,我们在/Core/Inc里创建bsp.h
bsp.h
//
// Created by Whisky on 2023/1/8.
//
#ifndef HELLOWORLD_BSP_H
#define HELLOWORLD_BSP_H
#include "sys.h"
#include "retarget.h"
#include "delay.h"
#include "led.h"
#define bsp_init() { delay_init(72); \
RetargetInit(&huart1); \
led_init(); \
}
#endif //HELLOWORLD_BSP_H
并在main.h中引用它
/* USER CODE BEGIN Includes */
#include "bsp.h"
/* USER CODE END Includes */
这样我们后面需要添加什么初始化和引用头文件就可以在bsp.h统一添加。