外部中断
外部中断的寄存器配置:
#include "key.h"
void Key_Init(void){
//打开gpiof和afio的时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
//配置输入模式,上下拉
GPIOF->CRH &= ~GPIO_CRH_MODE10;
GPIOF->CRH |= GPIO_CRH_CNF10_1;
GPIOF->CRH &= ~GPIO_CRH_CNF10_0;
//给下拉,初始值为低电平
GPIOF->ODR &= ~GPIO_ODR_ODR10;
//配置afio的引脚复用,告诉来中断的是具体哪个GPIOx
AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI10_PF;
//配置exti,的触发方式和是否中断屏蔽
EXTI->RTSR |= EXTI_RTSR_TR10;
EXTI->IMR |= EXTI_IMR_MR10;
//NVIC,这里用的库函数,自己手动配置寄存器也可以,就是有点复杂,直接调库
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(EXTI15_10_IRQn,15);
NVIC_EnableIRQ(EXTI15_10_IRQn);
}
//中断服务程序
void EXTI15_10_IRQHandler(void){
//先清除中断挂起标志位
EXTI->PR |= EXTI_PR_PR10;
//消抖
Delay_ms(10);
if ((GPIOF->IDR &= GPIO_IDR_IDR10) != RESET){
LED_Toggle(LED1);
}
}
hal库
void EXTI15_10_IRQHandler(void)
{
/* USER CODE BEGIN EXTI15_10_IRQn 0 */
/* USER CODE END EXTI15_10_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
/* USER CODE BEGIN EXTI15_10_IRQn 1 */
/* USER CODE END EXTI15_10_IRQn 1 */
}
/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
//是否是exti10传来的中断请求
if(GPIO_Pin == KEY1_Pin){
//清除中断挂起位,hal已经在底层默认清除了
HAL_Delay(10);
if (HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_SET)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
}
}
}
/* USER CODE END 1 */
上面的代码是在hal库里面的,在EXTI15_10_IRQHandler中有个
HAL_GPIO_EXTI_IRQHandler里面有个回调函数,并且自动清除了挂起寄存器,回调函数就是可以重自定义,并且他能接收一个引脚,这样就可以单独写出来,判断引脚到底是哪个,然后进行执行中断代码。
串口
在串口中我们单片机是usart,在电脑上的串口一般是RS232和RS485,其实底层就是usart只是,增加了电气标准,rs485更是发送信号变为了两根线,差分电路,抗干扰能力变得更强了,但是变为半双工了,如果分别两根线发和收,一共就四根线了,就是RS422了
波特率的产生
25.3.4
假设我们要一个115200的波特率
72000000/(115200*6)=39.0625
39的16进制位0x27(前12位)
0.0625*16 = 1(后四位小数部分)
轮询的方式
寄存器实现:
#include "usart.h"
void USART_Init(void){
//配置时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;//打开gpio
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;//打开串口1
//gpio工作模式
//pa9:复用推挽输出,CNF-10,MODE-11;
//PA10:浮空输入,CNF-01,MODE-00;
GPIOA->CRH |=GPIO_CRH_MODE9;
GPIOA->CRH |=GPIO_CRH_CNF9_1;
GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
GPIOA->CRH &=~GPIO_CRH_MODE10;
GPIOA->CRH &=~GPIO_CRH_CNF10_1;
GPIOA->CRH |= GPIO_CRH_CNF10_0;
//串口配置
//波特率设置
USART1->BRR = 0x271;
//使能
USART1->CR1 |= (USART_CR1_UE | USART_CR1_TE | USART_CR1_RE);
//其他配置
USART1->CR1 &=~USART_CR1_M;
USART1->CR1 &=~USART_CR1_PCE;
USART1->CR2 &=~USART_CR2_STOP;
}
void USART_SendChar(uint8_t ch){
//判断sr里的txe是否为1
while ((USART1->SR & USART_SR_TXE) == 0)
{}
//向DR写入新的要发送的数据
USART1->DR = ch;
}
uint8_t USART_ReceiveChar(void){
while ((USART1->SR & USART_SR_RXNE) == 0)
{}
return USART1->DR;
}
发送接收字符串
void USART_SendString(uint8_t *str , uint8_t size){
for (uint8_t i = 0; i < size; i++)
{
USART_SendChar(str[i]);
}
}
void USART_ReceiveString(uint8_t buffer[],uint8_t *size){
uint8_t i =0;
while (1)
{
while ((USART1->SR & USART_SR_RXNE) == 0)
{
if ((USART1->SR & USART_SR_IDLE))
{
*size = i;
return;
}
}
buffer[i] = USART1->DR;
i++;
}
}
hal库
uint8_t buffer[100] = {0};
while (1)
{
if (HAL_UART_ == HAL_OK)
{
HAL_UART_Transmit(&huart1,buffer,10,1000);
}
定长字符串发送
变长字符串的发送
HAL_UARTEx_ReceiveToIdle
一直接收到空间帧为止
uint8_t buffer[100] = {0};
uint16_t size =0;
while (1)
{
if (HAL_UARTEx_ReceiveToIdle(&huart1,buffer,100,&size,HAL_MAX_DELAY) == HAL_OK)
{
HAL_UART_Transmit(&huart1,buffer,size,HAL_MAX_DELAY);
}
}
printf重定向
打印在串口
printf底层主要是fputc这个,更改一下就可以重定向
去keil里面选择,这个一定要勾选,才能使用stdio.h
\\重写fputs函数,usart.c
int fputc(int ch,FILE * file){
USART_SendChar(ch);
return ch;
}
\\main.c
printf("c=%d\n",c);
printf("hello,world\t\n");
这样就实现了寄存器的打印方法。
hal库print重定向
//usart.h同样要引入stdio.h
int fputc(int ch,FILE * file){
HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,1000);
return ch;
}
小tick
既要收又要发的时候,GPIO必须配置为复用输出模式(推挽或者开漏),比如I2C
I2C协议
协议上是
高位先行,跟usart不一样,usart是低位先行
开始了后,第一步先传送设备的地址,但是在最后一位还要有方向
软件模拟IC2协议,只有寄存器能做
清除缓冲区的函数
meset(buffer,0,sizeof(buffer));
实现很麻烦,还是用硬件吧,解放cpu
\\i2c.c
#include "i2c.h"
// 初始化
void I2C_Init(void)
{
// 1. 配置时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
// 2. GPIO工作模式配置:复用开漏输出 CNF-11,MODE-11
GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11 |
GPIO_CRH_CNF10 | GPIO_CRH_CNF11);
// 3. I2C2配置
// 3.1 硬件工作模式
I2C2->CR1 &= ~I2C_CR1_SMBUS;
I2C2->CCR &= ~I2C_CCR_FS;
// 3.2 选择输入的时钟频率
I2C2->CR2 |= 36;
// 3.3 配置CCR,对应数据传输速率100kb/s,SCL高电平时间为 5us
I2C2->CCR |= 180;
// 3.4 配置TRISE,SCL上升沿最大时钟周期数 + 1
I2C2->TRISE |= 37;
// 3.5 使能I2C2模块
I2C2->CR1 |= I2C_CR1_PE;
}
// 发出起始信号
uint8_t I2C_Start(void)
{
// 产生一个起始信号
I2C2->CR1 |= I2C_CR1_START;
// 引入一个超时时间
uint16_t timeout = 0xffff;
// 等待起始信号发出
while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout)
{
timeout--;
}
return timeout ? OK : FAIL;
}
// 设置接收完成之后发出停止信号
void I2C_Stop(void)
{
I2C2->CR1 |= I2C_CR1_STOP;
}
// 主机设置使能应答信号
void I2C_Ack(void)
{
I2C2->CR1 |= I2C_CR1_ACK;
}
// 主机设置使能非应答信号
void I2C_Nack(void)
{
I2C2->CR1 &= ~I2C_CR1_ACK;
}
// 主机发送设备地址,并等待应答
uint8_t I2C_SendAddr(uint8_t addr)
{
// 直接将要发送的地址给到DR
I2C2->DR = addr;
// 等待应答
uint16_t timeout = 0xffff;
while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout)
{
timeout--;
}
// 访问SR2,清除ADDR标志位
if (timeout > 0)
{
I2C2->SR2;
}
return timeout ? OK : FAIL;
}
// 主机发送一个字节的数据(写入),并等待应答
uint8_t I2C_SendByte(uint8_t byte)
{
// 1. 先等待DR为空,上一个字节数据已经发送完毕
uint16_t timeout = 0xffff;
while ((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout)
{
timeout--;
}
// 2. 将要发送的字节放入DR中
I2C2->DR = byte;
// 3. 等待应答
timeout = 0xffff;
while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout)
{
timeout--;
}
return timeout ? OK : FAIL;
}
// 主机从EEPROM接收一个字节的数据(读取)
uint8_t I2C_ReadByte(void)
{
// 1. 先等待DR为满
uint16_t timeout = 0xffff;
while ((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout)
{
timeout--;
}
// 2. 将收到的字节数据返回
return timeout ? I2C2->DR : FAIL;
}
m24c02.c
/*
* @Author: wushengran
* @Date: 2024-05-31 11:48:48
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "m24c02.h"
// 初始化
void M24C02_Init(void)
{
I2C_Init();
}
// 向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr, uint8_t byte)
{
// 1. 发出开始信号
I2C_Start();
// 2. 发送写地址
I2C_SendAddr(W_ADDR);
// 3. 发送内部地址
I2C_SendByte(innerAddr);
// 4. 发送具体数据
I2C_SendByte(byte);
// 5. 发出一个停止信号
I2C_Stop();
// 延迟等待写入周期结束
Delay_ms(5);
}
// 读取EEPROM的一个字节
uint8_t M24C02_ReadByte(uint8_t innerAddr)
{
// 1. 发出开始信号
I2C_Start();
// 2. 发送写地址(假写)
I2C_SendAddr(W_ADDR);
// 3. 发送内部地址
I2C_SendByte(innerAddr);
// 4. 发出开始信号
I2C_Start();
// 5. 发送读地址(真读)
I2C_SendAddr(R_ADDR);
// 6. 设置非应答
I2C_Nack();
// 7. 设置在接收下一个字节后发出停止信号
I2C_Stop();
// 8. 读取一个字节
uint8_t byte = I2C_ReadByte();
return byte;
}
// 连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)
{
// 1. 发出开始信号
I2C_Start();
// 2. 发送写地址
I2C_SendAddr(W_ADDR);
// 3. 发送内部地址
I2C_SendByte(innerAddr);
// 利用循环不停发送数据
for (uint8_t i = 0; i < size; i++)
{
// 4. 发送具体数据
I2C_SendByte(bytes[i]);
}
// 5. 发出一个停止信号
I2C_Stop();
// 延迟等待写入周期结束
Delay_ms(5);
}
// 连续读取多个字节
void M24C02_ReadBytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)
{
// 1. 发出开始信号
I2C_Start();
// 2. 发送写地址(假写)
I2C_SendAddr(W_ADDR);
// 3. 发送内部地址
I2C_SendByte(innerAddr);
// 4. 发出开始信号
I2C_Start();
// 5. 发送读地址(真读)
I2C_SendAddr(R_ADDR);
// 利用循环连续读取多个字节
for (uint8_t i = 0; i < size; i++)
{
// 6. 设置应答或非应答
if (i < size - 1)
{
I2C_Ack();
}
else
{
I2C_Nack();
// 7. 设置发出停止信号
I2C_Stop();
}
// 8. 读取一个字节
buffer[i] = I2C_ReadByte();
}
}
hal库
/*
* @Author: wushengran
* @Date: 2024-05-31 11:48:48
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "m24c02.h"
// 初始化
void M24C02_Init(void)
{
MX_I2C2_Init();
}
// 向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr, uint8_t byte)
{
HAL_I2C_Mem_Write(&hi2c2,W_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,&byte,1,1000);
HAL_Delay(5);
}
// 读取EEPROM的一个字节
uint8_t M24C02_ReadByte(uint8_t innerAddr)
{
uint8_t byte;
HAL_I2C_Mem_Read(&hi2c2,R_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,&byte,1,1000);
return byte;
}
// 连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)
{
HAL_I2C_Mem_Write(&hi2c2,W_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,bytes,size,1000);
// 延迟等待写入周期结束
HAL_Delay(5);
}
// 连续读取多个字节
void M24C02_ReadBytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)
{
HAL_I2C_Mem_Read(&hi2c2,R_ADDR,innerAddr,I2C_MEMADD_SIZE_8BIT,buffer,size,1000);
}
定时器
systick定时器
理论上要配置nvic但是,systick是内核的,这就不用配置优先级了。
void SysTick_init(void){
SysTick->LOAD = 7999999;
SysTick->CTRL &= ~SysTick_CTRL_CLKSOURCE;
SysTick->CTRL |= SysTick_CTRL_TICKINT;
SysTick ->CTRL |= SysTick_CTRL_ENABLE;
}
void SysTick_Handler(void){
if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG)
{
LED_Toggle(LED1);
}
}
这里默认的就是用的72M的时钟源 SysTick->CTRL &= ~SysTick_CTRL_CLKSOURCE置0就是8M,1就是72M
在hal库中底层有一个全局变量 uwTick是以毫秒为单位计时。但是这个变量跟其他的一些系统参数有关,不能改变他的大小,这个时候获取一秒就可以在中断函数中
if (uwIick %1000 == 0){
led_toggle();
}
systick定时器只能向下计数,只有24位。
基本定时器
只能向上计数,只有16位。,没有外部IO,只能记时。
初始化配置
#include "timer6 .h"
void Timer_Init(void){
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
TIM6->PSC = 7199;
TIM6->ARR = 9999;
// 更新中断使能
TIM6->DIER |= TIM_DIER_UIE;
//配置中断
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(TIM6_IRQn,5);
NVIC_EnableIRQ(TIM6_IRQn);
// 开启定时器
TIM6->CR1 |= TIM_CR1_CEN;
}
void TIM6_IRQHandler(void){
TIM6->SR &= ~TIM_SR_UIF;//清除标志位,这个得软件清除,不能硬件清除
LED_Toggle(LED1);
}
这样就可以实现每秒翻转一下led灯了
使用hal库就是在中断的时候,选择回调函数,
选择PeriodElapsedCallback();
然后直接翻转引脚
void TIM6_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim6);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if (htim->Instance == TIM6)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
}
}
但是这样发现没有效果,这里有个坑
配置完了定时器,但是没有打开定时器,默认是不打开的,所以要使能。
这里用了中断就选图上那个。
现在就可以了
在做练习的时候(systick和tim6同时控制两个灯同时亮灭的时候)
有个坑
就是他们会不同步,每次都是tim6先进入中断
在底层是因为,定时器默认初始的时候,会产生一个刷新事件,会导致一次中断,所以他会。
有个思路就是引入一个变量,表示进入中断的次数,只要第一次进入中断的时候不翻转电平就可以了。
但是这种方法也有点low,最好的方法就是 底层主要是因为一开始他的ARR和psc刷不进去,所以以1/72M的速度进了一次中断,翻转了一次,所以也根本看不出来,既然是刷不技巧怒,那就直接用一个寄存器,可以手动刷进去,产生一个刷新事件,那就是
TIMx_EGR中的UG标志位
通用寄存器
基本定时器的功能全都可以,而且除了内部时钟源,还能选择外部时钟源。
从外部输入的引脚脉冲来作为时钟源,
外部时钟源模式1的通道1 的TI1FP1,和通道2的TI2FP2,还能做输入捕获
外部时钟源模式2 的TITMx_ETR直接就是直接外部输入的引脚脉冲来作为时钟源
PWM(脉冲宽度调制
三个重要参数
- 周期
- 频率
- 占空比:高电平宽度t除以周期T(占空比才会影响亮度,一般不改变周期和频率,这样才能改变等效的电压值)
输出比较功能
一般用来控制输出方波的、
大多用于输出pwm波
也可以输出其他波形
但是只能是方波
寄存器
pwm模式1
pwm模式2
以后常用的就是pwm1的模式,小于的时候是高电平。
配置:
#include "timer5.h"
void TIM5_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
//2.
GPIOA->CRL |= GPIO_CRL_MODE1;
GPIOA->CRL |= GPIO_CRL_CNF1_1;
GPIOA->CRL &= ~GPIO_CRL_CNF1_0;
//3.定时器配置
TIM5->PSC = 7199;
TIM5->ARR = 99;
TIM5->CR1 &= ~TIM_CR1_DIR;//默认是向上计数,这步不要也行
//设置通道2的CCR2的值
TIM5->CCR2 =50;
//配置通道2为输出模式
TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;
//配置通道2pwm1模式
TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;
TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;
TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0;
//开启使能输出通道
TIM5->CCER |= TIM_CCER_CC2E;
}
void TIM5_Start(void){
TIM5->CR1 |= TIM_CR1_CEN;
}
void TIM5_Stop(void){
TIM5->CR1 &= ~TIM_CR1_CEN;
}
void Set_DutyCycle(uint8_t dutycycle){
TIM5->CCR2 = dutycycle;
}
主函数实现
/*
* @Author: wushengran
* @Date: 2024-05-23 15:14:48
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "usart.h"
#include "m24c02.h"
#include <string.h>
#include "delay.h"
#include "timer5.h"
uint32_t count = 0;
int main(void)
{
TIM5_Init();
USART_Init();
printf("hello,world\n");
TIM5_Start();
uint8_t dutycycle = 0;
uint8_t dir = 0;
// 无限循环
while (1)
{
if (dir == 0)
{
/* 占空比增大*/
dutycycle +=1;
if (dutycycle >=99)
{
dir = 1;
}
}
else
{
/* 占空比增大*/
dutycycle -=1;
if (dutycycle <= 1)
{
dir = 0;
}
}
//设置占空比
Set_DutyCycle(dutycycle);
Delay_ms(10);
}
}
hal库
void SetDuty(uint8_t duty){
__HAL_TIM_SetCompare(&htim5,TIM_CHANNEL_2,duty);//这个函数设置占空比的
}
然后在主函数里面,需要开启pwm
HAL_TIM_PWM_Start(&htim5,TIM_CHANNEL_2);
LCD液晶屏背光源
在PB0上
测量PWM的频率和周期(输入捕获)
需要让定时器的时钟要大一点,然后LOAD也要大一点,直接取65535最大,防止测不出来
hal库默认选择的TIM4的CH1通道使用的引脚不对修改方法
直接选中他默认的引脚,在芯片图中,点击reset,然后选择要使用的引脚tim4复用,然后再重新配置。
hal库中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){
if(htim->Instance == TIM4){
__HAL_TIM_SetCounter(&htim4,0);//计数器清零
}
}
输入捕获一种特殊的模式——PWM输入模式
这种模式不仅可以测量周期,还可以测量占空比。
从模式控制定时器复位。
第一通道TI1FP1高电平触发,同时触发从模式,TI1FP2低电平触发。
高级定时器
相比于通用定时器多了
- 重复计数器
- 互补输出(只要ch1,ch2,ch3有,ch4没有)
- 死区生成
生成死区时间,延迟一段时间再开通。
- 短路输入信号
DMA模块
ME2ME(特殊)(ROM到RAM)
寄存器
DMA1.c文件
#include"dma.h"
//初始化
void DMA1_Init(void){
//开启时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
//数据传输方向
DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM;
DMA1_Channel1->CCR |= DMA_CCR1_DIR;
//设置传输的数据宽度,默认是8位
DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE;
DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE;
//开启地址自增
DMA1_Channel1->CCR |= DMA_CCR1_PINC;
DMA1_Channel1->CCR |= DMA_CCR1_MINC;
//开启数据传输完成中断标志
DMA1_Channel1->CCR |= DMA_CCR1_TCIE;
//开启中断服务nvic配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(DMA1_Channel1_IRQn,4);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
//数据传输
void DMA1_Transmit(uint32_t srcAddr,uint32_t destAddr,uint16_t datalen){
//1.设置外设地址
DMA1_Channel1->CPAR |= destAddr;
//2.设置存储器地址
DMA1_Channel1->CMAR |= srcAddr;
//3.设置传输的数据量
DMA1_Channel1->CNDTR = datalen;
//4.开启通道,开始传输数据
DMA1_Channel1->CCR |= DMA_CCR1_EN;
}
//中断服务程序
void DMA1_Channel1_IRQHandler(void){
//判断一下中断标志位
if (DMA1->ISR & DMA_ISR_TCIF1)
{
//清除中断标志位
DMA1->IFCR |= DMA_IFCR_CTCIF1;
//关闭DMA通道
DMA1_Channel1->CCR &= ~DMA_CCR1_EN;
DMA1_flag = 1;
}
}
main.c文件
/*
* @Author: wushengran
* @Date: 2024-05-23 15:14:48
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "led.h"
#include "delay.h"
#include "usart.h"
#include "string.h"
#include "stdio.h"
#include "dma.h"
uint8_t buff[100]={0};
uint8_t size = 0;
uint8_t DMA1_flag = 0;
//定义全局常量放在rom中作为数据源
const uint8_t src[] = {10,20,30,40};
//定义数据变量用来存储接收到的数据
uint8_t dest[4] = {0};
int main(void)
{
USART_Init();
DMA1_Init();
//打印变量常量地址,看看看在哪里
printf("src = %p, dest = %p ",src,dest);
//开启 dma通道进行传输
DMA1_Transmit((uint32_t)src,(uint32_t)dest,4);
while(1)
{
if (DMA1_flag)
{
for (uint8_t i = 0; i < 4; i++)
{
printf("%d\t",dest[i]);
}
DMA1_flag = 0;
}
}
}
hal库
it.c
void DMA1_Channel1_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Channel1_IRQn 0 */
/* USER CODE END DMA1_Channel1_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_memtomem_dma1_channel1);
/* USER CODE BEGIN DMA1_Channel1_IRQn 1 */
HAL_DMA_Abort_IT(&hdma_memtomem_dma1_channel1);//关闭DMA通道
flag = 1;
/* USER CODE END DMA1_Channel1_IRQn 1 */
}
main.c
printf("hello,world\n");
printf("src=%p\tdest=%p\n",src,dest);
//开启DMA传输
HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1,(uint32_t)src,(uint32_t)dest,4);
while (1)
{
if (flag)
{
for (uint8_t i = 0; i < 4; i++)
{
printf("%d\n",dest[i]);
}
flag = 0;
}
}
RAM到外设(串口)
寄存器
dma.c
/*
* @Author: wushengran
* @Date: 2024-06-07 11:35:48
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "dma.h"
// 初始化
void DMA1_Init(void)
{
// 1. 开启时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. DMA相关配置
// 2.1 数据传输方向: 从存储器读,发往串口外设
DMA1_Channel4->CCR |= DMA_CCR4_DIR;
// 2.2 数据宽度: 8位 - 00
DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;
// 2.3 地址自增:开启自增,串口地址不能自增
DMA1_Channel4->CCR &= ~DMA_CCR4_PINC;
DMA1_Channel4->CCR |= DMA_CCR4_MINC;
// 2.4 开启数据传输完成中断标志
DMA1_Channel4->CCR |= DMA_CCR4_TCIE;
// 2.5 使能串口的DMA传输功能
USART1->CR3 |= USART_CR3_DMAT;
// 2.6 开启循环传输功能
DMA1_Channel4->CCR |= DMA_CCR4_CIRC;
// 3. NVIC配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(DMA1_Channel4_IRQn, 2);
NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
// 数据传输
void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen)
{
// 1. 设置外设地址
DMA1_Channel4->CPAR = destAddr;
// 2. 设置存储器地址
DMA1_Channel4->CMAR = srcAddr;
// 3. 设置传输的数据量
DMA1_Channel4->CNDTR = dataLen;
// 4. 开启通道,开始传输数据
DMA1_Channel4->CCR |= DMA_CCR4_EN;
}
// 中断服务程序
void DMA1_Channel4_IRQHandler(void)
{
// 判断中断标志位
if (DMA1->ISR & DMA_ISR_TCIF4)
{
// 清除中断标志位
DMA1->IFCR |= DMA_IFCR_CTCIF4;
// 关闭DMA通道
// DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
}
}
main.c
/*
* @Author: wushengran
* @Date: 2024-05-23 15:14:48
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "usart.h"
#include "dma.h"
#include "delay.h"
// 定义变量数组,放置在RAM中,用来存储接收到的数据
uint8_t src[4] = {'a','b','c','d'};
int main(void)
{
// 初始化
USART_Init();
DMA1_Init();
printf("Hello, world!\n\n");
Delay_ms(1);
// 开启DMA通道进行传输
DMA1_Transmit((uint32_t)src, (uint32_t)&(USART1->DR), 4);
while(1)
{}
}
hal库
uint8_t str[] = {'a','b','c','d','e'};
HAL_UART_Transmit_DMA(&huart1,str,5);
配置好后,两行代码搞定。
ADC
寄存器
adc.c
#include "adc.h"
// 初始化
void ADC1_Init(void)
{
// 1. 时钟配置
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// CFGR:ADCPRE - 10,6分频,得到 12MHz
RCC->CFGR |= RCC_CFGR_ADCPRE_1;
RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;
// 2. GPIO工作模式,PC0,模拟输入,00 00
GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
// 3. ADC配置
// 3.1 工作模式:禁用扫描模式
ADC1->CR1 &= ~ADC_CR1_SCAN;
// 3.2 启用连续转换模式(单曲循环)、
ADC1->CR2 |= ADC_CR2_CONT;
// 3.3 数据右对齐(默认)
ADC1->CR2 &= ~ADC_CR2_ALIGN;
// 3.4 设置通道10的采样时间,001 - 7.5个时钟周期
ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;
// 3.5 规则组通道序列配置
// 3.5.1 规则组中的通道个数 L
ADC1->SQR1 &= ~ADC_SQR1_L;
// 3.5.2 将通道号 10 保存到序列中的第一位
ADC1->SQR3 &= ~ADC_SQR3_SQ1;
ADC1->SQR3 |= 10 << 0;
// 3.5 选择软件触发AD转换
// ADC1->CR2 |= ADC_CR2_EXTTRIG;
// ADC1->CR2 |= ADC_CR2_EXTSEL; // 选择的就是SWSTART控制AD转换
}
// 开启转换
void ADC1_StartConvert(void)
{
// 1. 上电唤醒
ADC1->CR2 |= ADC_CR2_ADON;
// 2. 执行校准
ADC1->CR2 |= ADC_CR2_CAL;
// 等待校准完成
while (ADC1->CR2 & ADC_CR2_CAL)
{}
// 3. 启动转换
// ADC1->CR2 |= ADC_CR2_SWSTART;
ADC1->CR2 |= ADC_CR2_ADON;
// 4. 等待转换完成
while ((ADC1->SR & ADC_SR_EOC) == 0)
{}
}
// 返回转换后的模拟电压值
double ADC1_ReadV(void)
{
return ADC1->DR * 3.3 / 4095;
}
main.c
#include "usart.h"
#include "adc.h"
#include "delay.h"
int main(void)
{
// 初始化
USART_Init();
ADC1_Init();
printf("Hello, world!\n");
// 开启AD转换
ADC1_StartConvert();
while(1)
{
// 向串口发送打印转换结果
printf("V = %.2f\n", ADC1_ReadV());
Delay_ms(1000);
}
}
hal库
printf("hello,world\n");
//开启校准
HAL_ADCEx_Calibration_Start(&hadc1);
//启动ADC
HAL_ADC_Start(&hadc1);
while (1)
{
// 获取转换结果,进行计算
double v = HAL_ADC_GetValue(&hadc1)*3.3/4095;
printf("%.2f",v);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
tips:
在GPIO中输入模式可以不用配置时钟,因为底层的电路就是通的
要把获取结果放到while循环中并且打印,不能把获取结果放在while外面,然后再轮询打印。
校准需要时间的。
SPI
SP1_W25Q32芯片(NOR FLASH)
软件模拟
w25q32.c
/*
* @Author: wushengran
* @Date: 2024-06-12 15:54:23
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "w25q32.h"
// 初始化
void W25Q32_Init(void)
{
SPI_Init();
}
// 读取ID
void W25Q32_ReadID(uint8_t *mid, uint16_t *did)
{
SPI_Start();
// 1. 发送指令 9fh
SPI_SwapByte(0x9f);
// 2. 获取制造商ID(为了读取数据,发送什么不重要)
*mid = SPI_SwapByte(0xff);
// 3. 获取设备ID
*did = 0;
*did |= SPI_SwapByte(0xff) << 8;
*did |= SPI_SwapByte(0xff) & 0xff;
SPI_Stop();
}
// 开启写使能
void W25Q32_WriteEnable(void)
{
SPI_Start();
SPI_SwapByte(0x06);
SPI_Stop();
}
// 关闭写使能
void W25Q32_WriteDisable(void)
{
SPI_Start();
SPI_SwapByte(0x04);
SPI_Stop();
}
// 等待状态不为忙(busy)
void W25Q32_WaitNotBusy(void)
{
SPI_Start();
// 发送读取状态寄存器指令
SPI_SwapByte(0x05);
// 等待收到的数据末位变成0
while (SPI_SwapByte(0xff) & 0x01)
{
}
SPI_Stop();
}
// 擦除段(sector erase),地址只需要块号和段号
void W25Q32_EraseSector(uint8_t block, uint8_t sector)
{
// 首先等待状态不为忙
W25Q32_WaitNotBusy();
// 开启写使能
W25Q32_WriteEnable();
// 计算要发送的地址(段首地址)
uint32_t addr = (block << 16) + (sector << 12);
SPI_Start();
// 发送指令
SPI_SwapByte(0x20);
SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
SPI_SwapByte(addr >> 8 & 0xff); // 第二个字节
SPI_SwapByte(addr >> 0 & 0xff); // 第三个字节
SPI_Stop();
W25Q32_WriteDisable();
}
// 写入(页写)
void W25Q32_PageWrite(uint8_t block, uint8_t sector, uint8_t page, uint8_t *data, uint16_t len)
{
// 首先等待状态不为忙
W25Q32_WaitNotBusy();
// 开启写使能
W25Q32_WriteEnable();
// 计算要发送的地址(页首地址)
uint32_t addr = (block << 16) + (sector << 12) + (page << 8);
SPI_Start();
// 发送指令
SPI_SwapByte(0x02);
// 发送24位地址
SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
SPI_SwapByte(addr >> 8 & 0xff); // 第二个字节
SPI_SwapByte(addr >> 0 & 0xff); // 第三个字节
// 依次发送数据
for (uint16_t i = 0; i < len; i++)
{
SPI_SwapByte(data[i]);
}
SPI_Stop();
W25Q32_WriteDisable();
}
// 读取
void W25Q32_Read(uint8_t block, uint8_t sector, uint8_t page, uint8_t innerAddr, uint8_t *buffer, uint16_t len)
{
// 首先等待状态不为忙
W25Q32_WaitNotBusy();
// 计算要发送的地址
uint32_t addr = (block << 16) + (sector << 12) + (page << 8) + innerAddr;
SPI_Start();
// 发送指令
SPI_SwapByte(0x03);
// 发送24位地址
SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
SPI_SwapByte(addr >> 8 & 0xff); // 第二个字节
SPI_SwapByte(addr >> 0 & 0xff); // 第三个字节
// 依次读取数据
for (uint16_t i = 0; i < len; i++)
{
buffer[i] = SPI_SwapByte(0xff);
}
SPI_Stop();
}
spi.c
#include "spi.h"
// 初始化
void SPI_Init(void)
{
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 2. GPIO工作模式
// PA5、PA7、PC13:通用推挽输出,CNF = 00,MODE = 11
GPIOC->CRH |= GPIO_CRH_MODE13;
GPIOC->CRH &= ~GPIO_CRH_CNF13;
GPIOA->CRL |= GPIO_CRL_MODE5;
GPIOA->CRL &= ~GPIO_CRL_CNF5;
GPIOA->CRL |= GPIO_CRL_MODE7;
GPIOA->CRL &= ~GPIO_CRL_CNF7;
// PA6:MISO,浮空输入,CNF = 01,MODE = 00
GPIOA->CRL &= ~GPIO_CRL_MODE6;
GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
GPIOA->CRL |= GPIO_CRL_CNF6_0;
// 3. 选择SPI的工作模式 0:SCK空闲0
SCK_LOW;
// 4. 片选不使能
CS_HIGH;
// 5. 延时
SPI_DELAY;
}
// 数据传输的开始和结束
void SPI_Start(void)
{
CS_LOW;
}
void SPI_Stop(void)
{
CS_HIGH;
}
// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte)
{
// 定义变量保存接收到的字节
uint8_t rByte = 0x00;
// 用一个循环,依次交换8位数据
for (uint8_t i = 0; i < 8; i++)
{
// 1. 先准备要发送的数据(最高位),送到MOSI
if (byte & 0x80)
{
MOSI_HIGH;
}
else
{
MOSI_LOW;
}
// 左移一位
byte <<= 1;
// 2. 拉高时钟信号,形成一个上升沿
SCK_HIGH;
SPI_DELAY;
// 3. 在MISO上采样Flash传来的数据
// 先做左移
rByte <<= 1;
if (MISO_READ)
{
rByte |= 0x01;
}
// 4. 拉低时钟,为下次数据传输做准备
SCK_LOW;
SPI_DELAY;
}
return rByte;
}
main.c
#include "usart.h"
#include "w25q32.h"
#include <string.h>
int main(void)
{
// 1. 初始化
USART_Init();
W25Q32_Init();
printf("尚硅谷SPI软件模拟实验开始...\n");
// 2. 读取ID进行测试
uint8_t mid = 0;
uint16_t did = 0;
W25Q32_ReadID(&mid, &did);
printf("mid = %#x, did = %#x\n", mid, did);
// 3. 段擦除
W25Q32_EraseSector(0, 0);
// 4. 页写
W25Q32_PageWrite(0, 0, 0, "12345678", 8);
// 5. 读取
uint8_t buffer[10] = {0};
W25Q32_Read(0, 0, 0, 2, buffer, 6);
printf("buffer = %s\n", buffer);
while (1)
{
}
}
FSMC(灵活的静态存储器控制器)
在我们扩展的sram里面想写入数据进行测试,怎么生成的变量指定放到扩展区域呢
可以使用关键字attribute
uint8_t v1 __attribute__ ((at(0x68000000)));
v1 = 10 ;
//必须是全局变量,只能单独声明,不能同时赋值,给的地址必须是4的整数,内存对齐