使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容博客作业5 中断与UART
零、 创新点
《嵌入式技术与基础 第六版》原书教材随书附赠了一块stm32L431的开发板,配套内容都是以此为基础打造的,甚至作者还专门为此开发了一套ide,但是:
- 现在stm32主流的教程生态用的都是stm32f103c8t6这款芯片,原书的教程虽然能很好的为你解释嵌入式开发的原理,但是要进一步学习,你还是不得不寻找其他教程,购买主流的开发板
- 作者的ide完全是出于本书学习设计的,对于实际开发毫无意义,若将来想做点嵌入式,还需要重新学习一套开发栈
- 全新套书卖100。作为抠门带血生,肯定要考虑二手的,但是二手书通常不会带有原书的附件。单买开发板,某宝60,某鱼甚至没有。起码截至我使用该书学习的时间(2024年3月)是这样的,后期使用此书的前辈多了,估计二手市场会变好。
- 最重要的是,我已经有一个stm32f103c8t6的开发板了
综上,无论是从学习角度还是省钱角度,使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容都是非常合适的
这套博客是广州大学计算机学院嵌入式系统课程的作业,我用stm32f103c8t6开荒,也是为了后来的同学做贡献
一、 从一个简单的串口实验说起
①在电脑的输出窗口显示下一个字符,如收到A显示B;
②亮灯:收到字符G,亮绿灯;收到字符R,亮红灯;收到字符B,亮蓝灯;收到其他字符,不亮灯。
实现方式:
以下是使用hal库实现功能的核心代码:
/* USER CODE BEGIN 2 */
char receive_char[1];
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_UART_Receive(&huart2, (uint8_t*)&receive_char, 1,HAL_MAX_DELAY);
switch(receive_char[0]){
case 'R':
HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_SET);
break;
case 'G':
HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_SET);
break;
case 'Y':
HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_SET);
break;
default:
receive_char[0] = receive_char[0] + 1;
HAL_UART_Transmit(&huart2,(uint8_t*)receive_char, 1,HAL_MAX_DELAY);
HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_RESET);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
串口操作都浓缩到这两行代码里
HAL_UART_Receive(huart, pData, Size, Timeout);//接收
HAL_UART_Transmit(huart, pData, Size, Timeout);//发送
甚至在原书的金葫芦ide中,代码已经做了printf重定向,直接使用printf就能打印,和普通的c语言编程没什么区别
printf("这是金葫芦ide");
不过,从底层来看,可没那么简单,而且串口通信能做的远超这个
我们可以使用中断实现这个代码逻辑:当uart收到信息时,触发中断,运行相应代码
在实际应用中,为了提高效率和实时性,通常会利用中断机制来管理发送和接收过程。当发生发送完成、接收准备好或错误事件时,相应的中断标志会被置位,通过中断服务程序来响应这些事件,执行相应的数据处理或错误恢复操作。
二、 UART的底层原理
在软件层面,实现STM32 UART通信主要涉及以下几个关键步骤和原理:
1. 初始化UART外设
在使用UART之前,首先需要通过编程配置其相关寄存器来初始化UART外设:
-
选择USART和时钟源:根据项目需求选择合适的USART,并确保其对应的时钟已经使能。
-
波特率设置:波特率决定了数据传输的速度,计算并设置USART_BRR寄存器以得到所需的波特率。公式通常为
USARTDIV = f_PCLK / (USART_BAUDRATE * 16)
其中f_PCLK是USART的输入时钟频率。
-
数据位、停止位和奇偶校验设置:通过USART_CR1和USART_CR2寄存器配置数据帧的格式,比如8位数据位、1位停止位、无奇偶校验或有奇偶校验等。
-
使能发送和接收:设置USART_CR1中的TXEIE(发送数据寄存器空中断使能)和RXNEIE(接收数据寄存器非空中断使能),以及TE(发送使能)和RE(接收使能)位。
//相应寄存器地址
volatile uint32_t* rcc = (volatile uint32_t*)0x40021000UL; //时钟寄存器基地址
volatile uint32_t* rcc_ahb2 = (volatile uint32_t*)((uint32_t)rcc | 0x4CUL); //AHB2总线外设时钟使能寄存器基地址
volatile uint32_t* rcc_apb1 = (volatile uint32_t*)((uint32_t)rcc | 0x58UL); //APB1总线外设时钟使能寄存器基地址
volatile uint32_t* gpioa = (volatile uint32_t*)0x48000000UL; //gpioa寄存器基地址
volatile uint32_t* gpioa_moder = gpioa; //gpioa模式寄存器基地址
volatile uint32_t* gpioa_afrl = gpioa + 8; //gpioa复用功能低位寄存器基地址
volatile uint32_t* usart2 = (volatile uint32_t*)0x40004400UL; //usart2寄存器基地址
volatile uint32_t* usart2_cr1 = usart2; //usart2控制寄存器1基地址
volatile uint32_t* usart2_cr2 = usart2 + 1; //usart2控制寄存器2基地址
volatile uint32_t* usart2_cr3 = usart2 + 2; //usart2控制寄存器3基地址
volatile uint32_t* usart2_brr = usart2 + 3; //usart2波特率寄存器基地址
volatile uint32_t* nvic_iser = (volatile uint32_t*)0xE000E100UL; //nvic中断设置使能寄存器基地址
//用户外设模块初始化
gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF); //初始化红灯
gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF); //初始化绿灯
gpio_init(LIGHT_BLUE,GPIO_OUTPUT,LIGHT_OFF); //初始化蓝灯
//配置usart2复用
//1、使能gpioa和UART2的时钟
*rcc_ahb2 |= (0x1UL<<0U); //gpioa时钟使能
*rcc_apb1 |= (0x1UL<<17U); //UART2时钟使能
//2、gpioa 端口设置成 usart 复用模式
//2.1 配置gpioa模式寄存器为复用模式(两个引脚)
*gpioa_moder &= ~((0x3UL<<4U)|(0x3UL<<6U));
*gpioa_moder |= ((0x2UL<<4U)|(0x2UL<<6U));
//2.2 配置gpioa复用功能寄存器
*gpioa_afrl &= ~((0xFUL<<8U)|(0xFUL<<12U));
*gpioa_afrl |= ((0x7UL<<8U)|(0x7UL<<12U));
//3、关闭 usart
*usart2_cr1 &= ~(0x1UL);
//4、关闭串口的收发功能
*usart2_cr1 &= ~((0x1UL<<2U)|(0x1UL<<3U));
//5、配置 usart 模式
//5.1 配置数据位长度(8位)
*usart2_cr1 &= ~((0x1UL<<12U)|(0x1UL<<28U));
//5.2 配置过采样模式(16)
*usart2_cr1 &= ~(0x1UL<<15U);
//5.3 配置是否启用校验和校验类型(禁用奇偶校验控制)
*usart2_cr1 &= ~(0x1UL<<10U);
//5.4 配置usart_CR2,将使能位清零。D14—LIN模式使能位、D11—时钟使能位
*usart2_cr2 &= ~((0x1UL<<14U)|(0x1UL<<11U));
//5.5 配置usart_CR3,将控制寄存器3的三个使能位清零。D5 (SCEN) —smartcard模式使能位、D3 (HDSEL) —半双工选择位、D1 (IREN) —IrDA 模式使能位
*usart2_cr3 &= ~((0x1UL<<5U)|(0x1UL<<3U)|(0x1UL<<1U));
//6、配置波特率因子
uint16_t usartDIV = (uint16_t)((SystemCoreClock/115200));
*usart2_brr = usartDIV;
//7、打开串口的收发功能
*usart2_cr1 |= ((0x1UL<<2U)|(0x1UL<<3U));
//8、打开 usart
*usart2_cr1 |= 0x1UL;
2. 接收数据
接收数据流程主要包括:
- 等待接收数据:软件监控USART_SR寄存器的RXNE位(接收数据寄存器非空标志)。当RXNE=1,表示有新的数据可供读取。
- 读取数据:从USART_DR寄存器中读取接收到的数据字节。
- 处理接收完成:可设置接收中断(RXNEIE),每当接收到新数据时,处理器将被中断唤醒,以便及时处理接收到的信息。
if((*usart2_cr1)&(0x1UL<<5U)) //判断是否使能接收缓冲区非空中断
{
for (uint32_t i = 0; i < 0xFFFF; ++i)//查询指定次数
{
if((*usart2_isr)&(0x1UL<<5U)) //判断读取数据寄存器是否非空
{
data = *usart2_rdr; //读取接收寄存器
switch(data){
case 'R':gpio_set(LIGHT_RED,LIGHT_ON);break;
case 'G':gpio_set(LIGHT_GREEN,LIGHT_ON);break;
case 'B':gpio_set(LIGHT_BLUE,LIGHT_ON);break;
default:
gpio_set(LIGHT_RED,LIGHT_ON);
gpio_set(LIGHT_BLUE,LIGHT_OFF);
gpio_set(LIGHT_GREEN,LIGHT_OFF);
for (uint32_t j = 0; j < 0xFFFF; ++j)//查询指定次数
{
if((*usart2_isr)&(0x1UL<<7U)) //判断发送数据寄存器是否为空
{
*usart2_tdr = data + 1; //回发接收到的内容(内容加一)
break;
}
}//end for
}
break;
}
}
}
三、 中断
1. 中断的基本概念
中断是处理器对外部事件或内部事件的一种响应机制。当一个中断发生时,CPU会暂停当前正在执行的任务,保存其状态,然后跳转到特定的中断服务程序地址去执行相应的处理代码。处理完成后,CPU会恢复之前任务的状态并继续执行。
2. STM32中断体系结构
STM32微控制器基于ARM Cortex-M系列内核,通常使用嵌套向量中断控制器(NVIC)来管理中断。NVIC支持优先级管理,可以实现中断的嵌套,即高优先级中断可以打断低优先级中断的处理。
3. 中断配置与使能
在软件层面,使用STM32需要通过编程来配置和使能中断。这包括以下几个步骤:
- 中断源配置:根据需要处理的外部或内部事件,配置相应的外设或寄存器,比如GPIO的边沿触发、定时器的溢出等。
- 中断优先级设置:在NVIC中为每个中断分配优先级。STM32支持多个优先级等级,可以通过编程设置。
- 中断使能:在NVIC中使能特定中断,允许它能够打断CPU的正常流程。
//配置usart2中断使能
//1、配置usart2接收缓冲区非空中断使能
*usart2_cr1 |= (0x1UL<<5U);
//2、使能NVIC中USART2的中断
*(nvic_iser + (USART2_IRQn >> 5U)) |= (0x1UL << ((uint32_t)USART2_IRQn & 0x1FUL));
4. 中断向量表
Cortex-M系列处理器使用嵌套向量中断控制器(NVIC),并有一个中断向量表存储在内存的固定位置。这个表列出所有中断的服务程序入口地址。当一个中断发生时,CPU会根据中断号从向量表中取出对应的地址,然后跳转到该地址执行ISR。
g_pfnVectors:
#略
.word USART1_IRQHandler
.word USART2_IRQHandler #我们的中断处理函数
.word USART3_IRQHandler
#略
.word 0
.word BootRAM
5. 中断服务程序(ISR)
ISR是用户编写的处理特定中断的函数。为了快速响应,ISR通常要求简短且高效。进入ISR后,CPU会自动保存一些关键寄存器,然后执行ISR中的代码。ISR完成处理后,一般会通过特定指令恢复现场并返回,以便CPU继续执行被中断的任务。
6. 中断的进入与退出
- 进入中断:中断发生时,CPU保存当前状态(如寄存器值),根据中断向量表跳转到ISR执行。
- 退出中断:ISR执行完毕后,通过恢复之前保存的CPU状态(如程序计数器PC),CPU回到中断前的执行点继续运行。
四、封装下的实现
介绍完了寄存器编程方式,我们来看点阳间的实现
先在stm32 cube ide中配置引脚,我们以uart2为例
启用uart2中断
我们让ide生成代码,然后就可以在Core/Src/stm32f1xx_it.c
中看到这个
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
/* USER CODE END USART2_IRQn 0 */
HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
/* USER CODE END USART2_IRQn 1 */
}
系统生成的HAL_UART_IRQHandler(&huart2);
用于帮我们完成最基本的中断处理,比如让中断寄存器重制避免无限中断
别和我一样一股脑得就想在这里完成自己的代码,因为能触发串口中断的有很多,例如校验错误等,不一定是我们要的
HAL库替我们考虑好了,如果是普通的接收中断,HAL_UART_IRQHandler(&huart2);
函数会调用HAL_UART_RxCpltCallback(UART_InitTypeDef *huart)
函数,所以我们只需要重新定义这个函数void HAL_UART_RxCpltCallback(UART_InitTypeDef *huart);
就可以实现自定义收到数据后的中断处理流程
char receive_char[1];//因为有多个函数要访问receive_char,所以定义为全局变量
void HAL_UART_RxCpltCallback(UART_InitTypeDef *huart){
switch(receive_char[0]){
case 'R':
HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_SET);
break;
case 'G':
HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_SET);
break;
case 'Y':
HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_SET);
break;
default:
receive_char[0] = receive_char[0] + 1;
HAL_UART_Transmit(&huart2,(uint8_t*)receive_char, 1,HAL_MAX_DELAY);
HAL_GPIO_WritePin(YELLO_GPIO_Port, YELLO_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(RED_GPIO_Port, RED_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GREEN_GPIO_Port, GREEN_Pin, GPIO_PIN_RESET);
}
HAL_UART_Receive_IT(&huart2, (uint8_t*)&receive_char, 1,HAL_MAX_DELAY);//为下次接收开启中断
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
HAL_UART_Receive_IT(&huart2, (uint8_t*)&receive_char, 1,HAL_MAX_DELAY);
while (1);
}
输入R、Y后红灯黄灯亮,符合预期效果
关于作者:
- 邮箱:luokairui@carry.fit
- 个人博客:carry blog
- CSDN主页:_:Carry-CSDN博客
- Github主页:C-a-r-r-y
欢迎联系!