这里以STM32G030C8T6为例,别的型号的32单片机大同小异
学会单片机,无非就是学会通过寄存器去控制GPIO接口、USART串口通信、中断系统、时钟系统、ADC数模转换、DMA。通过大致了解配置寄存器的原理,大部分功能可直接使用HAL库函数来实现。以此为基础根据外设需求进行拓展。
GPIO:通用输入输出接口
顾名思义,就是通过配置单片机相应引脚为输入或者输出模式来读取外设传来的数据或者给外设输出相应的数据,无论是输入模式还是输出模式所能做的操作都是高低电平的操作(及0、1操作)
根据使用场景不同,输入模式可分为:
悬空模式:当无外界输入时引脚的电平为不确定值,引脚电平高低完全由外界输入决定(最常用)
上拉模式:当无外界输入时引脚的电平为高电平,有外界输入时由外界输入决定
下拉模式:当无外界输入时引脚的电平为第电平,有外界输入时由外界输入决定
模拟模式:专门用于接收外界传入的模拟信号
同样,输出模式可分为:
开漏输出:只能输出低电平
推挽输出:高低电平都能输出(最常用)
如:假设LED接在PB2引脚,输出低电平时能让LED发光
首先通过STM32CubeMAX配置一个空工程
配置寄存器:
1、开启GPIOB组的时钟(使能):
RCC->IOPENR |= 1<<1 ; //使能GPIOB组时钟
2、配置PB2引脚为输出模式
GPIOB->MODER &= ~(1<<5); //配置PB2为输出模式
GPIOB->MODER |= 1<<4;
3、配置为推挽输出类型
GPIOB->OTYPER &= ~(1<<2); //推挽输出
4、低电平输出
GPIOB->ODR &= ~(1<<2); //输出低电平
即将:
RCC->IOPENR |= 1<<1 ; //使能GPIOB组时钟
GPIOB->MODER &= ~(1<<5); //配置PB2为输出模式
GPIOB->MODER |= 1<<4;
GPIOB->OTYPER &= ~(1<<2); //推挽输出
GPIOB->ODR &= ~(1<<2); //输出低电平
写入main.c中,烧录至单片机中即可点亮该LED
通过手动配置寄存器的方式来使引脚输出低电平比较麻烦,下面是通过CubeMAX来配置并使用HAL函数来控制引脚的输入输出
B组2号引脚输出低电平:HAL_GPIO_WritePin(GPIOB,GPIO_PIN_2,GPIO_PIN_RESET)
B组2号引脚输出高电平: HAL_GPIO_WritePin(GPIOB,GPIO_PIN_2,GPIO_PIN_SET)
多个引脚输出:HAL_GPIO_WritePin(GPIOB,GPIO_PIN_2 | GIOP_PIN_3,GPIO_PIN_SET)
读B组2号引脚:HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)
电平翻转:HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_2 | GPIO_PIN_3)
USART串口通信
USART:通用同步异步穿行收发器
通过串口助手来实现与单片机的收发,注意要波特率一致
STM32G030C8T6型号单片机上有两个此收发器
USATR1:PA9发送,PA10接收
USART2:PA2发送,PA3接收
现通过CubeMAX配置USART1
生成工程
发送单个字符 :
USART1->TDR = ch; //数据发送寄存器
以此为基础封装成函数
当快速不间断的发送字符时由于移位寄存器读取数据速度远小于CPU写入速度,导致未被发送的数据被新写入的数据覆盖。出现顺序混乱
如何优化:
只需让CPU在向TDR寄存器中写入新数据时,判断是否可以写入,也就是等待发送寄存器为空时,TDR中的数据已经被移位寄存器拿走,此时则可以写入新的要发送的数据。
优化后的代码如下(即可发送字符串):
接收单个字符:
ch = USART1->RDR; //数据接收寄存器
同发送多个字符一样也存在数据覆盖的问题,解决方法同上
解决数据覆盖的问题后即可接收字符串,代码如下:
相关的HAL函数:
发送字符串:HAL_UART_Transmit(&huart1,"ikun",sizeof("ikun"),500)
接收字符串:HAL_UART_Receive(&huart1,buf,sizeof(buf),500)
(第四个参数为超时检测,防止数据量未达到指定长度时一直阻塞)
中断系统:
中断就是对突发事件的处理过程
当遇到内部/外部的紧急事件需要处理时,暂时中止当前程序,转而去处理紧急事件,
待处理完毕后,再返回被打断的程序继续向下运行。
处理器中负责处理中断的是NVIC(嵌套中断控制器)
负责外部中断的是EXIT
如:主程序LED不断翻转,当按下按键的时候发生中断,给串口发送字符串(按钮接PA8)
用CubeMAX配置引脚:
配置LED引脚
配置串口通信引脚
外部中断使能
主函数:
外部中断回调:(基于GPIO功能的外部中断回调)
时钟系统:
又分为systick定时器、定时器、PWM等内容
systick定时器:
原理是通过对晶振的计数,使其1ms产生一次中断
该处理器内部原始晶振频率为16MHZ,波形为方波,一个上升沿计一个数的话,计一个数就需要t=1/(16*10^3)ms,即1ms需要计16000个数,接下来了解一下相关的寄存器
不理解没关系,上实例!!!
如自己定义一个ms精度级的延时函数:
void delay_ms(int t)
{
SysTick->LOAD=16000*t-1;//计数1000个为1毫秒,因为是从0开始计数所以要减1
SysTick->VAL=0;//为了减少误差,当前值寄存器归零
while(!(SysTick->CTRL & 1<<16));//延时等待,检查SysTick控制寄存器中的COUNTFLAG位 是否为1,是则表示计数器已经溢出。
}
定时器:
定时器和systick定时器类似,区别在于定时器可以使用分频器
由计数器、自动重装寄存器、预分频器组成
本质就是计数器
STM32G030C8T6有以下的定时器:
定时器计数原理:
时钟频率配置成了48Mhz,如何让定时器产生1s中断?
分频值写0相当于不分频 48/1 => 48
先对主频进行48分频得到1Mhz的频率,则分频值为48-1
48Mhz / 48 => 1Mhz
1Mhz的时钟频率,相当于计一个数需要1/1000000秒,
所以如果想得到1s中断,则需要从0开始计数到1000000-1,即计1000000个数需要1秒。
1000000个数 * 1/1000000s = 1s
频率是时间的倒数 1Mhz = 1/1000000s
实验:利用定时器中断实现1s打印一个“hellowolrd”
配置串口通信:
配置定时器,且由时钟树选择合适的分频:
定时器中断使能:
定义中断回调函数(尽量别放在main.c中):
PWM:
定义:PWM(Pulse Width Modulation)简称脉宽调制,是利用微处理器的数字输出来对模拟电 路进行控制的一种非常有效的技术。
工作原理:通过调节高电平在一个电平周期内的占空比来精准调整输出信号的电平平均值
计数器寄存器 (TIMx_CNT)
自动装载寄存器 (TIMx_ARR)
捕获/比较寄存器(TIMx_CCRx)
向上计数模式:
输出过程:
当0-t1这段时间,计数器寄存器的CNT的值是小于CCR,输出高电平。
当t1-t2这段时间,计数器寄存器的CNT的值是大于CCR且小于ARR的,输出低电平。
当CNT的值达到ARR里的值时,产生溢出事件,自动清零再次从0开始向上计数。
应用:电机控制、照明控制、音频处理、无线通信、电池管理等
实验:通过PWM信号调节LED灯亮度
开启谐振器:
选择合适的频率来配置时钟树:
选择PWM信号的输出引脚:
选择合适得分频数和重装数(使其配置为1s(自动重装计数器应为1000-1)):
main.c调用:
开启:
通过配置初始计数值来控制高电平的占空比:
ADC:
数模转换器
用于将模拟信号转化为数字信号
只有一个ADC1,但有多个通道
有:单通道单次转换模式、单通道连续转换模式、多通道扫描单次转换模式、多通道扫描连续转 换模式四种工作模式
实验:采集光敏传感器的模拟量(光敏传感器的模拟量输出接到PA4串口),数据发送到串口助手
选用ADC1的IN4通道,即选择PA4引脚的功能为模拟量输入:
使能串口
相关HAL库函数:
1.启动ADC
HAL_StatusTypeDef HAL_ADC_Start (ADC_HandleTypeDef * hadc)
功能:启动ADC开始转换
参数:ADC_HandleTypeDef * hadc 句柄
返回值: 状态
2.等待转换结束
HAL_StatusTypeDef HAL_ADC_PollForConversion (ADC_HandleTypeDef * hadc, uint32_t Timeout)
功能:等待转换完成
参数: ADC_HandleTypeDef * hadc 句柄
uint32_t Timeout 超时时间
返回值好:转换状态
3.获取转换结果
uint32_t HAL_ADC_GetValue (ADC_HandleTypeDef * hadc)
功能:获取转换结果
参数:ADC_HandleTypeDef * hadc 句柄
返回值:转换结果
4.停止ADC
HAL_StatusTypeDef HAL_ADC_Stop (ADC_HandleTypeDef * hadc)
功能:停止ADC
参数:ADC_HandleTypeDef * hadc 句柄
返回值: 状态
很好,我也看不懂,上代码!
若要输出具体电压值:value = HAL_ADC_GetValue(&hadc1)*3.3/4095
(不一定是3.3,应为处理器的供电电压)
其中printf函数需要重定向(注意头文件需要加stdio.h文件):
int fputc(int ch, FILE * p)
{
while(!(USART1->ISR & (1<<7)));
USART1->TDR=ch;
return ch;
}
以上是单通道采集,接下来用多通道采集电池电压模拟量和光敏传感器模拟量
选用ADC1的两个通道IN1和IN4
选择序列和扫描模式:
ADCx_ISR 中断和状态寄存器
看不懂没关系,上代码
由此可见多通道采集是一个接一个的按通道顺序单个采集
DMA—数据的搬运工:
- DMA作用
DMA的传输方式无需CPU参与,可以直接控制传输
DMA给外部设备和内存开辟了一条直接数据传输的通道
- 目的
给CPU节省资源,使CPU的工作效率提高
- DMA特性
1)同一个DMA模块可以有多个优先级请求:很高 高 中等 低
2)每个通道有3个事件标志: DMA半传输 DMA传输完成 DMA传输出错
3)数据源 目标源 数据传输宽度对齐
4)传输数据 字节8位 半字16位 全字32位
5)存储器<->存储器 外设<->存储器 外设<->外设
6)闪存(flash) SRAM APB AHB 外设均可以作为源或者目标
7)搬移数据的最大长度为65535字节
- DMA寄存器
DMA_CPARx :设置外设地址的寄存器
DMA_CMARx :设置存储器地址的寄存器
DMA_CCRx :设置数据传输方向
DMA_CNDTRx:设置传输的数据量
- DMA的增量或者循环模式
增量: 外设搬移到存储器的时候,不希望覆盖上一个会将内存设置为增量模式
循环: DMA不停循环的搬移数据,一组的数据传输完成时,计数寄存器将会自动地被恢复成配置该通道时设置的初值.
太罗嗦了,上代码
依旧是光敏传感器数据采集:
为串口ADC配置DMA:
通信串口同上,这里不再赘述
main.c:
uint16_t buf;//全局变量
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&buf, 1);//写到main函数里
while (1)
{
HAL_Delay(500);//写不写都可以
}
adc.c:
#include <stdio.h>
#include "usart.h"
/* USER CODE END 0 */
extern uint16_t buf; // 光照强度缓冲区
int fputc(int ch, FILE* f)//重定向
{
while ((USART1->ISR & 1<<7) == 0){}
USART1->TDR = ch;
return ch;
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) //ADC转换完成时即触发
{
if (hadc->Instance == ADC1)
{
HAL_ADC_Stop_DMA(&hadc1);//停止搬运
printf("光照强度为: %d", buf);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&buf,1);//开启搬运
}
}
至此,STM32单片机的基础内容已大致熟悉,不同型号的单片机HAL库函数可能会有些差异,建议使用前应先查找手册。
通过向工程中添加各个外设模块的驱动文件,配合上外设使用,这就能逐渐拿捏STM32单片机了!