非专业,但对嵌入式微控制器以及01感到些许兴趣,特学习一番。
文章目录
目录
前言
- 于51之后,最近学习STM32(stm32f103c8单片机最小系统板)。
- 写下这篇文章,记录学过的一些东西。
- 本章主要结合内容: 按键开关、双轴摇杆模块的ADC模数转换、输出PWM到51开发板步进电机模块控制小马达、STM32与51串口通信发送信息、OLED显示屏 ...
- 由于缺少电机驱动模块,恰好利用51开发板上的步进电机模块,同时实现串口通信就可以在51上的数码管或者LCD1602显示屏显示更多变量信息。
一、硬件接线
二、实现效果
- 视频: 由于摇杆模块精度问题,模量变化太快,实验不太理想。
STM32与51开发板电机驱动+PWM+串口通信
三、步骤
1.两个软件分别写程序
STM32使用的库函数编程,可以到B站找大佬视频学习和理解,在此不加以解释。明白这些存在,运用这些功能,乐哉乐哉。
- Keil5 C51软件——51单片机
- Keil5 MDK软件——STM32
2.51程序
- main.c :51开发板主要就接收来自STM32的串口数据(速度显示在1/2/3数码管)。而P1^0到P1^4已经与步进电机模块连接,直接在P1^0脚上输入PWM就可以了,这里电机的一端接01引脚,另一端接5V引脚(因为驱动能力问题电机一端必须要以5V供电,这就导致无法软件改变正反转,只有调速效果了),不过通过P1^6和P1^7两个引脚接正反转信号,在数码管上显示也足以判断。
#include <REGX52.H> #include "Delay.h" #include "Nixie.h" #include "UART.H" sbit PWM=P1^0; sbit AIN1=P1^6; sbit AIN2=P1^7; unsigned char Information; void main() { UART_INT1_Init(); while(1) { Nixie(5,PWM); //第五数码管:PWM输入变化显示 Nixie(7,AIN1); //第七数码管:正反转判断1显示 Nixie(8,AIN2); //第八数码管:正反转判断2显示 Nixie(1,Information/100); //一:速度百位 Nixie(2,(Information%100)/10); //二:速度十位 Nixie(3,(Information%100)%10); //三:速度个位 } } /** * @brief 串口中断服务程序(当STM32发送数据过来,INT1脚有高电平,"RI=0"且产生串口中断,51单片机开始接收数据读到(SBUF)接收缓冲寄存器) * @param 无参数 * @retval 无返回值 * @danger 类型 * */ void UART_Routine() interrupt 4 //+串口中断号 { if(RI==1) //如果接受完毕 { Information = SBUF; RI=0; //必须清零 } }
- Uart.h :P3_0脚接收串口数据,P3^1脚串口发送。
#include <REGX52.H>
/**
* @brief 对串口初始化,串口传输“波特率”=4800bps,@12.00MHz
* @param 无输入参数
* @retval 无返回值
* @danger 单片机发送得和PC端接收到的数据不一样,最有可能是波特率设置的误差所造成
UART通信传输的是HEX模式(即字符对应的ASC2码值)。
*/
void UART_INT1_Init(void)
{
SCON = 0x50; //(SCON)串行控制寄存器:0101 0000 ,
//(SM0=0,SM1=1:工作方式一,REN位=1:允许串行口接受数据)
PCON |= 0x80; //(PCON)波特率选择寄存器:1000 0000 , (使用串行通信方式1的波特率)波特率加倍
TMOD &= 0x0F; //(TMOD)定/计器工作模式寄存器:0000 xxxx,(该“与运算”不影响“定/计器0”的模式)
//把“定/计器1”用做定时模式工作,且只有INT1(P3.3)脚为高及TR1为1时才打开“定/计器1”
TMOD |= 0x20; //(TMOD)定/计器工作模式寄存器:0010 xxxx,(该“或运算”不影响“定/计器0”的状态),
//设置“定/计器1”为(定时)工作模式2,(M1=1,M0=0)8位自动重装载定时器,定时溢出时TH1的值自动重装入TL1里面。
TL1 = 0xF4; //设置了定时的初值
TH1 = 0xF4; //设置定时重装值,根据所需要的“波特率”计数
ET1 = 0; //禁止定时器T1中断
TR1 = 1; //定时器T1开始计时要素之一,另外还要等INT1脚为高电平
//------------串口接收时设置,允许串口中断且还要注意SCON串行寄存器得REN位也要置1,两个通信设备之间波特率设置没问题,就可以实现数据传输。
EA = 1;
ES = 1;
}
/**
* @brief 从INT1脚开始串口发送一个字节数据函数(将数据写到(SBUF)发送缓冲寄存器,共其他设备读取)
* @param 要发送的一个字节(8位)
* @retval 无返回值
* @danger 类型
*/
void UART_INT1_SendByte(unsigned char Byte)
{
SBUF=Byte; //_写数据(1字节=8个位)到(SBUF)缓存器
while(TI==0) ; //"TI"发送中断请求位表示检测是否发送完毕,(TI==1时)发送完退出循环
TI=0; //每次发送完一字节后,"TI"位必须软件清0,以准备重新装填发送
}
/**
* @brief 发送一串字符数据函数(字符串循环将串分成单个字节放到SBUF寄存器)
* @param 要发送的字符串首地址
* @retval 无返回值
* @danger 地址参数
*/
void UART_INT1_SendString(unsigned char *str)
{
while(*str != '\0')
{
UART_INT1_SendByte(*str);
str++;
}
}
3.STM32的程序:
-
main.c : 获取双轴按键摇杆XY轴的AD信号,这里只用Y轴控制电机。用独立按键至状态开启,则在摇杆向y轴摇动杆时速度(Speed)大小与捕获到AD量的关系见程序。 调用Motor的函数传入速度值改变输出的PWM。 OLED显示屏:1行、显示X轴ADC量;2行、显示Y轴ADC量;3行、显示正/反转Speed值 与 开启/关闭。 最后,结果将Speed值修正通过串口发送给51单片机。
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "YGADC.h" #include "SERIAL.h" #include "Motor.h" #include "Key.h" uint16_t ADCXvalue ,ADCYvalue; //X,Y变量 uint8_t KeyNum,on_of ; //按键,开关变量 int8_t Speed,SendSpeed; //速度档,串口发送速度变量 int main(void) { OLED_Init(); //初始化OLED显示屏 ACD_Init(); //初始化摇杆模块ADC转换 Serial_Init();//初始化串口通信 Motor_Init(); //初始化电机驱动 Key_Init(); //初始化按键 // Serial_SendString("Wellcome XY :\r\n"); OLED_ShowString(1,1,"ADCX:"); OLED_ShowString(2,1,"ADCY:"); while(1) { ADCXvalue=ADC_Getvalue(ADC_Channel_0); //返回值是通道0 ADCYvalue=ADC_Getvalue(ADC_Channel_1); //返回值是通道1 OLED_ShowNum(1,6,ADCXvalue,4); //OLED显示值 OLED_ShowNum(2,6,ADCYvalue,4); KeyNum = Key_GetNum(); if (KeyNum == 1) //如果按键按下,开启/关闭电机运动 { on_of++; on_of %= 2; } if(on_of == 1) //如果开启,将摇杆的模拟数字量分段,在每一段期间匹配一个决定电机速度的PWM { if(1900<=ADCYvalue && ADCYvalue<=2200) //停止转动 Speed = 0; else if(2200<ADCYvalue && ADCYvalue<=2560) //正转 Speed = 20; else if(2560<ADCYvalue && ADCYvalue<=2920) Speed = 40; else if(2920<ADCYvalue && ADCYvalue<=3280) Speed = 60; else if(3280<ADCYvalue && ADCYvalue<=3640) Speed = 80; else if(3640<ADCYvalue && ADCYvalue<=4100) Speed = 100; else if(1540<ADCYvalue && ADCYvalue<=1900) //反转 Speed = -20; else if(1180<ADCYvalue && ADCYvalue<=1540) Speed = -40; else if(820<ADCYvalue && ADCYvalue<=1180) Speed = -60; else if(160<ADCYvalue && ADCYvalue<=820) Speed = -80; else if(0<ADCYvalue && ADCYvalue<=160) Speed = -100; } Motor_SetSpeed(Speed); //产生PWM输出 OLED_ShowSignedNum(3, 2, Speed, 3); OLED_ShowNum(3, 10, on_of, 1); if(Speed > 0) //if+else 保证串口发送速度参数为正数 {SendSpeed = Speed;} else {SendSpeed = -Speed;} Serial_SendByte(SendSpeed); //串口发送 Delay_ms(100); //延时时间,决定着采样摇杆数据,以及串口发送的频率 } }
-
Motor.c : 电机速度(变量Speed)的正负值传入到CCR3,并由PA4和PA5脚( 推挽输出)决定正反转。
/****** (电机驱动) *******PBA2总线 —— 开启时钟 GPIOA *******输出脚:PA4 、PA5 ******* ******* */ #include "stm32f10x.h" // Device header #include "PWM.h" void Motor_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); PWM_Init(); } void Motor_SetSpeed(int8_t Speed) //正反转与速度大小 { if (Speed >= 0) { GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); PWM_SetCompare3(Speed); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_5); PWM_SetCompare3(-Speed); } }
-
PWM.c : TIM2时钟通道3装载CCR3值,由PA2脚(复用推挽输出)产生PWM输出。
/****** (PWM1输出) *******PBA2总线 —— 开启时钟 TIM2、GPIOA *******输出脚:PA2 ******* ******* */ #include "stm32f10x.h" // Device header void PWM_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_InternalClockConfig(TIM2); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; //PSC TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCStructInit(&TIM_OCInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; //CCR TIM_OC3Init(TIM2, &TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); } void PWM_SetCompare3(uint16_t Compare) { TIM_SetCompare3(TIM2, Compare); }
-
YGADC.c :摇杆模块ADC捕获程序,通过PA0和PA1脚(模拟输入)获取XY轴的模拟信号。
/****** (XYZ摇杆模块/模数转换1) *******PBA2总线 —— 开启时钟ADC1、GPIOA ******* 输入脚:PA0 、PA1 ******* ******* */ #include "stm32f10x.h" // Device header void ACD_Init(void) { //配置时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//开启ADC的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启引脚GPIOA时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6);//ADC分频器六分频 72/6=12hz //配置GPIOA GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //配置ADC结构体 ADC_InitTypeDef ADC_Initstructure; ADC_Initstructure.ADC_Mode=ADC_Mode_Independent; //模式配置 独立模式 ADC_Initstructure.ADC_DataAlign=ADC_DataAlign_Right; //对其模式 右对齐 ADC_Initstructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None; //出发控制的触发源 不需要外部触发 ADC_Initstructure.ADC_ContinuousConvMode=DISABLE; //连续模式 enable 连续 disable 单次 ADC_Initstructure.ADC_ScanConvMode=DISABLE; //扫描模式 enable扫描模式 disable非扫描 ADC_Initstructure.ADC_NbrOfChannel=1; //指定规则组通道的数目 ADC_Init(ADC1,&ADC_Initstructure); ADC_Cmd(ADC1,ENABLE); //ADC上电 //校准 ADC_ResetCalibration(ADC1);//复位校准 while(ADC_GetResetCalibrationStatus(ADC1)==SET);//返回复位校准的状态 如果没完成就一直循环 复位后会清零 ADC_StartCalibration(ADC1);//开始校准 while(ADC_GetCalibrationStatus(ADC1)==SET);//校准是否完成,如果没完成就一直循环 } uint16_t ADC_Getvalue(uint8_t ADC_Channel) //输入通道获取值 { //ADC通道配置 、输入通道可以获取值 ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);//在序列1写入通道0 时间55.5个ADC时间 ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发转换 while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);//获取标志位状态 规则组转换完成标志位 0转换未完成 1 转换完成 return ADC_GetConversionValue(ADC1); }
-
Key.c:按键开关,PB1脚(上拉输入)得到开关信号吧。
/****** (按键) *******PBA2总线 —— 开启时钟 GPIOB ******* 输入脚:PB1 ******* ******* */ #include "stm32f10x.h" // Device header #include "Delay.h" /***************************************************** *********函数:初始化PB1按键的读取端口********** *********返回值:无 ******************************************************/ void Key_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); } /************************************************ ***************函数:获取按键的状态******************** ***************返回值:相应按键变化(按下PB1返回:1 ) ************************************************/ uint8_t Key_GetNum(void) { uint8_t KeyNum = 0; if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0) { Delay_ms(20); while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0); Delay_ms(20); KeyNum = 1; } return KeyNum; }
-
Serials.c :串口通信(可发送可接收)。PA9脚发送(复用推挽输出)PA10接收数据(上拉输入)
/****** (串口通信1) *******PBA2总线 —— 开启时钟USART1、GPIOA *******输出脚Tx:PA9 读入脚Rx:PA10 ******* ******* */ #include "stm32f10x.h" // Device header #include <stdio.h> //printf() #include <stdarg.h> //sprintf() /****为串口中断产出的变量*****/ uint8_t serial_RxData; uint8_t serial_RxFlag; void Serial_Init(void) { //开启时钟:打开USART(1/2)和GPIOx(A/B/C) RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟(总线APB2上) RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟(TX1与RX1分在PA9与PA10脚) //GPIO初始化: TX配置复用输出,RX配置输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //对于TX脚是外设输出脚所以配复用推挽输出。RX脚是外设数据输入脚所以配输入模式 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //TX脚 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //对于RX脚是外设输入脚(IPD浮空输入或者IPU上拉输入)。 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //RX脚 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //配置USART: 一个结构体包含所需参数。 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 4800; //波特率 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制(不使用、只用CTS、只用RTS或者CTS、RST都用) USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式:发送功能_TX 或 接收功能_RX USART_InitStructure.USART_Parity = USART_Parity_No; //校验位(No无校验、Odd奇校验、Even偶校验) USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位(1、1.5、2) USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长:(8位/9位) USART_Init(USART1, &USART_InitStructure); //使能命令以后就可以直接(发送)和(查询接收)。(接收两种:1、主函数不断查询判断RXNE标志位;2、需另设中断)。 //接收:中断方式 USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); //RXNE标志位、使能 /******RXNE置1,串口可以开始申请中断************/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStrruct; NVIC_InitStrruct.NVIC_IRQChannel = USART1_IRQn; //中断通道 NVIC_InitStrruct.NVIC_IRQChannelCmd = ENABLE; //使能 NVIC_InitStrruct.NVIC_IRQChannelPreemptionPriority = 1; //优先级 NVIC_InitStrruct.NVIC_IRQChannelSubPriority = 1; // ··· NVIC_Init(&NVIC_InitStrruct); USART_Cmd(USART1, ENABLE); } //发送一个字节(8位) void Serial_SendByte(uint8_t Byte) { USART_SendData(USART1, Byte); //传送数据,写到TDR >>转移到移位寄存器 while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待 TXE标志位置1(下次会自动清零) } //发送数组(首地址、长度(16位或者32位)) void Serial_SendArray(uint8_t *Array, uint16_t Length) { uint16_t i; for (i = 0; i < Length; i ++) { Serial_SendByte(Array[i]); //依次发送数组的每一项 } } //发送字符串(char类型字符串首地址) void Serial_SendString(char *String) { uint8_t i; for (i = 0; String[i] != '\0'; i ++) { Serial_SendByte(String[i]); } } //数字取余运算(10、次数) //返回值:余数 uint32_t Serial_Pow(uint32_t X, uint32_t Y) { uint32_t Result = 1; while (Y --) { Result *= X; } return Result; } //发送字符串形式的数字(数值、长度) void Serial_SendNumber(uint32_t Number, uint8_t Length) { uint8_t i; for (i = 0; i < Length; i ++) { Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //传送取余数 } } //printf()打印函数的原型(单串口) //调用形式:printf("%d",变量); int fputc(int ch, FILE *f) { Serial_SendByte(ch); //重印象到TX脚输出 return ch; } //一次多个参数输出 //(格式化字符串、可变量参数x表) void Serial_Printf(char *format, ...) { char String[100]; va_list arg; //定义参数列表变量 va_start(arg, format); //从format接收放在arg里 vsprintf(String, format, arg); //(打印位置、格式化字符串、参数表) va_end(arg); //释放参数表 Serial_SendString(String); //发送 } //读到后自动清零 uint8_t Serial_GetRxFlag(void) { if(serial_RxFlag == 1) { serial_RxFlag = 0; return 1; //子函数执行一个return就退出 } return 0; } //封装变量 uint8_t Serial_GetRxData(void) { return serial_RxData ; } //串口中断服务函数 void USART1_IRQHandler(void) { if(USART_GetFlagStatus(USART1,USART_IT_RXNE) == SET) { serial_RxData = USART_ReceiveData(USART1); serial_RxFlag = 1; USART_ClearITPendingBit(USART1,USART_IT_RXNE); } }
-
OLED.h:OLED显示屏得相关函数。
#ifndef __OLED_H #define __OLED_H void OLED_Init(void); //初始化OLED void OLED_Clear(void); //清屏 void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char); //显示一个字符 void OLED_ShowString(uint8_t Line, uint8_t Column, char *String); //显示字符串 void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length); //显示数字 void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length); //显示带符号数字 void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length); //显示十六进制数 void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length); //显示二进制数 #endif
4.源链接
51源程序与32源程序
链接:https://pan.baidu.com/s/1UIiPXW8kY37truW2c5Sbeg?pwd=3251
提取码:3251
啥
因兴趣而学习,简单了解电子世界。虽不是大项目,通过独立小小实验还是明白嵌入式微控器的运作原理。
拿上STM32引脚定义表与参考手册,方可琢透一二。
链接:https://pan.baidu.com/s/1i1oX7ntdjK0oj9A8VgoazQ?pwd=3251
提取码:3251