STM32教程第三讲:串口调试技巧
文章目录
需求
1.设备上电后四个灯灭。
2.按下KEY1,LED1灯亮,同时串口发送“LED1灯亮”。
3.再次按下KEY1,LED1灯灭,同时串口发送“LED1灯灭”。
4.以此类推,设置KEY2,KEY3,KEY4以及对应的LED灯。
一、串口调试的意义
在linux和C语言的学习中,每当编译完程序时代码编译没错,但功能实现却出现错误时,我们通常会使用printf来打印一下,观察程序到底是运行到哪里出错了。
在STM32中,由于现阶段还没有学习屏幕显示,当你在程序中直接添加printf时,我们无法观察到具体现象。此时,学会如何通过串口来调试就显得及其重要了。
二、串口调试实现流程
1.开时钟
首先,打开原理图找到USART1(串口)的位置
打开数据手册,找到系统结构图,查看控制串口和引脚的时钟在那个总线。
由上图中可知,控制USART1和GPIOA的时钟均在APB2时钟总线上。
查看手册中APB2外设时钟使能寄存器是如何控制的:
由上图可知,控制USART1和GPIOA的时钟是由APB2外设时钟使能寄存器的第14位和第2位控制的。该模块均为1使能0关闭。
在代码中为:
//开时钟:GPIOA,USART1
RCC->APB2ENR |= 0x01<<2;//GPIOA
RCC->APB2ENR |= 0x01<<14;//USART1
2.配置对应的IO口
由于配置对应的IO口 PA9(TX)和 PA10(RX)都是高八位,所以直接打开手册找到端口配置高寄存器。
模式的确定是由手册中8.1.11 外设的GPIO配置确定的
通信方式:
串行:串行数据传输时,数据是一位一位地在通信线上传输的。
并行:并行通信传输中有多个数据位,同时在两个设备之间传输。
单工:设备只能发送,或者只能接收 只有一种功能,比如收音机。
双工:设备能够发送,也能够接收。
全双工:发送和接收可以独立工作,可以同时进行收发,互不影响。
半双工:发送的时候不能接收,接收的时候不能发送 比如对讲机。
同步:设备之间使用同一个时钟线。
异步:设备有自己的时钟,但是通信时时钟要设置成一样。
时钟作用:决定了通信的速率。
通常情况下我们使用的串口都是串行全双工异步通信接口。
所以此时我们要将PA9(TX)配置为复用推挽(1011),PA10(RX)配置成浮空输入(0100)
代码如下:
//配置对应的IO口
GPIOA->CRH &= ~(0x0f<<4);//PA9清0
GPIOA->CRH |= 0x0B<<4;//设置成复用推挽输出
GPIOA->CRH &= ~(0x0f<<8);//PA10清0
GPIOA->CRH |= 0x04<<8;//设置成浮空
3.配置串口1
串口1配置时需要设置数据位、校验位、停止位和波特率。
本次配置为8数据位,0校验位,1停止位,115200波特率
在手册中找到串口的控制寄存器CR1和CR2
在CR1中可以看到数据位在12位,校验位在第10位,发送使能在第3位,串口使能在第13位。
要注意的是发送使能和串口使能要最后再配制,防止没配置好串口就开始工作了。
在CR2中可以看到停止位在13和12位,此时全配1即可。
代码如下:
USART1->CR1 &= ~(0x01<<12);//数据位置0
USART1->CR1 &= ~(0x01<<10);//设置0位校验位
USART1->CR2 &= ~(0x03<<12);//设置1个停止位
接下来就要配置波特率了
波特率的配置是由波特比率寄存器决定的
DIV_Mantissa是USARTDIV的整数部分
DIV_Fraction[3:0]:USARTDIV的小数部分
计算方式为:
72M/usartdiv/16=115200
usartdiv = 72M/16/115200 = 39.0625
39.0625 = DIV_Mantissa + (DIV_Fraction/16)
DIV_Mantissa(整数部分) = 39
DIV_Fraction(小数部分) = 0.0625*16 = 1;
其中72M是由所在的时钟总线APB2决定的。
APB2总线默认时钟频率与系统时钟频率保持一致是72MHz。
此时,完整的串口1配置代码为:
//配置串口1 8数据位,0校验位,1停止位,波特率115200
USART1->CR1 &= ~(0x01<<12);//数据位置0
USART1->CR1 &= ~(0x01<<10);//设置0位校验位
USART1->CR2 &= ~(0x03<<12);//设置1个停止位
USART1->BRR = (39<<4)+1;//设置波特率
USART1->CR1 |= 0x01<<3;//发送使能
USART1->CR1 |= 0x01<<13;//使能串口1
此时的串口就完全配置好了,可以直接通过串口的数据寄存器发送一个8位的数据。
USART1->DR = 'A';
4.printf函数的重定向
配置完串口后,此时想要使用printf函数进行调试,还是不行的。
我们要将printf函数进行重定向,让打印的数据通过串口输出出来。
printf函数的重定向在官方给的标准库中是存在的,只需打开官方例程即可找到。
找到后复制过来即可:
int fputc(int ch, FILE *f)
{
//printf函数最终会跳转到这里来运行
while((USART1->SR&0x1<<6)==0);
//发送数据
USART1->DR = (uint8_t)ch;
return ch;
}
复制完后记得在头文件中声明一下。
完成以上步骤就可实现printf串口调试功能了。
三、需求的实现
需求均已实现,完整代码如下:
main.c
#include "stm32f10x.h"
#include "led.h"
#include "key.h"
#include "delay.h"
#include "usart.h"
#include "stdio.h"
int a,b,c,d;//LED灯的标志位
int main()
{
Led_Init();
key_Init();
Beep_Init();
Usart1_Config();
while(1)
{
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1)//key1
{
Delay_nms(10);
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1)
{
while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1)
{}
Led_Toggle(1);
a++;
if(a==2)
{
printf("LED1灯灭\r\n");
a=0;
}
else
{
printf("LED1灯亮\r\n");
}
}
}
if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_4)==0)//key2
{
Delay_nms(10);
if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_4)==0)
{
while(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_4)==0)
{}
Led_Toggle(2);
b++;
if(b==2)
{
printf("LED2灯灭\r\n");
b=0;
}
else
{
printf("LED2灯亮\r\n");
}
}
}
if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)==0)//key3
{
Delay_nms(10);
if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)==0)
{
while(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)==0)
{}
Led_Toggle(3);
c++;
if(c==2)
{
printf("LED3灯灭\r\n");
c=0;
}
else
{
printf("LED3灯亮\r\n");
}
}
}
if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_6)==0)//key4
{
Delay_nms(10);
if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_6)==0)
{
while(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_6)==0)
{}
Led_Toggle(4);
d++;
if(d==2)
{
printf("LED4灯灭\r\n");
d=0;
}
else
{
printf("LED4灯亮\r\n");
}
}
}
}
}
usart.c
#include "usart.h"
#include "stdio.h"
void Usart1_Config()
{
//开时钟:GPIOA,USART1
RCC->APB2ENR |= 0x01<<2;//GPIOA
RCC->APB2ENR |= 0x01<<14;//USART1
//配置对应的IO口 PA9(tx):复用推挽 PA10(RX):浮空输入
GPIOA->CRH &= ~(0x0f<<4);//PA9清0
GPIOA->CRH |= 0x0B<<4;//设置成复用推挽输出
GPIOA->CRH &= ~(0x0f<<8);//PA10清0
GPIOA->CRH |= 0x04<<8;//设置成浮空
//配置串口1 8数据位,0校验位,1停止位,波特率115200
USART1->CR1 &= ~(0x01<<12);//数据位置0
USART1->CR1 &= ~(0x01<<10);//设置0位校验位
USART1->CR2 &= ~(0x03<<12);//设置1个停止位
/*
72M/usartdiv/16=115200
usartdiv = 72M/16/115200 = 39.0625
39.0625 = DIV_Mantissa + (DIV_Fraction/16)
DIV_Mantissa(整数部分) = 39
DIV_Fraction(小数部分) = 0.0625*16 = 1;
0000 0010 0111 0001
*/
USART1->BRR = (39<<4)+1;//设置波特率
USART1->CR1 |= 0x01<<3;//使能串口发送
USART1->CR1 |= 0x01<<13;//使能串口1
}
void SendData(uint8_t data)
{
while((USART1->SR&0x01<<6)==0){}//等待上次发送完成
USART1->DR = data;//发送数据
}
int fputc(int ch, FILE *f)
{
//printf函数最终会跳转到这里来运行
while((USART1->SR&0x1<<6)==0);
//发送数据
USART1->DR = (uint8_t)ch;
return ch;
}
usart.h
#ifndef _USART_H_
#define _USART_H_
#include "stm32f10x.h"
#include "stdio.h"
void Usart1_Config();
void SendData(uint8_t data);
int fputc(int ch, FILE *f);
#endif
key.c
#include "stm32f10x.h"
void key_Init()
{
//开时钟
RCC->APB2ENR |= 0x01<<4;//PC
RCC->APB2ENR |= 0x01<<2;//PA
//配置模式
GPIOC->CRL &=~(0X0F << 24);//PC6 key4
GPIOC->CRL |= 0X04 << 24;
GPIOC->CRL &=~(0X0F << 20);//PC5 key3
GPIOC->CRL |= 0X04 << 20;
GPIOC->CRL &=~(0X0F << 16);//PC4 key2
GPIOC->CRL |= 0X04 << 16;
GPIOA->CRL &=~0X0F;//PA0 key1
GPIOA->CRL |= 0X04;
}
int Get_Key_Val(void)
{
int key_val = 0;
if(!!(GPIOA->IDR &(0X01 << 0))==1)
key_val = 1;
if(!!(GPIOC->IDR &(0X01 << 4))==0)
key_val = 2;
if(!!(GPIOC->IDR &(0X01 << 5))==0)
key_val = 3;
if(!!(GPIOC->IDR &(0X01 << 6))==0)
key_val = 4;
return key_val;
}
key.h
#ifndef _KEY_H_
#define _KEY_H_
void key_Init();
int Get_Key_Val(void);
#endif
led.c
#include "stm32f10x.h"
void Led_Init()
{
//配置好模式,然后全灭
//开APB2时钟
RCC->APB2ENR |= 0X01 << 6;
//配置PE2--PE5为通用推挽输出
GPIOE->CRL &=~(0X0F << 20);//PE5
GPIOE->CRL |= 0X03 << 20;
GPIOE->CRL &=~(0X0F << 16);//PE4
GPIOE->CRL |= 0X03 << 16;
GPIOE->CRL &=~(0X0F << 12);//PE3
GPIOE->CRL |= 0X03 << 12;
GPIOE->CRL &=~(0X0F << 8);//PE2
GPIOE->CRL |= 0X03 << 8;
//4个引脚均输出高电平
GPIOE->ODR |= (0x0F << 2);
}
//开关灯
void Led1_Ctrl(int flag)
{
if(!!flag)
{
GPIOE->ODR &= ~(0x0F << 2);
}
else
{
GPIOE->ODR |= (0x0F << 2);
}
}
void Led_Toggle(int flag)
{
GPIOE->ODR ^= 0x01<<(flag+1);
}
led.h
#ifndef _LED_H_
#define _LED_H_
void Led_Init();
void Led1_Ctrl(int flag);
void Led_Toggle(int flag);
#endif
delay.c
#include "stm32f10x.h"
#include "delay.h"
void Delay_nus(uint32_t time)
{
uint32_t i=0;
for(i=0;i<time;i++){
delay1us();
}
}
void Delay_nms(uint32_t time)
{
uint32_t i=0;
for(i=0;i<time;i++){
Delay_nus(1000);//延时1ms
}
}
delay.h
#ifndef _DELAY_H_
#define _DELAY_H_
#include "stm32f10x.h"
#define delay1us() {__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();\
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();}
void Delay_nus(uint32_t time);
void Delay_nms(uint32_t time);
#endif
总结
1.了解多种通信方式和作用。
2.学会了如何在固件库中查找以及使用官方例程。
3.学会了USART1的配置及使用,以后就可以对程序直接进行printf调试了。
作者:小白橘颂