本例程采用HAL项目开发(主要使用软件)CubexMX和keil5)文章末尾会有代码开源,欢迎指正讨论。
一、 硬件模块和原理图
1、硬件组成
硬件组成:stm32f103c8t6最小系统板;0.96寸LED12864(I2C通信模式);智能汽车12v移动电源;25GA370直流减速电机(带霍尔编码器);JDY-31蓝牙模块;L298N电机驱动模块;多条杜邦线;一个面包板;
图片如下:
2、模块分析
1、L298N电机驱动模块
1.模块可驱动两个直流电机输出A和B连接一直流电机即可;
2.若使用12V供电,将12V供电端口及GND连接正负电源,同时5V供电端可作为最小系统板的输入电源;
三、若无需使用PWM调速,只需控制电机正反转,逻辑A和B跳线帽插上即可,相当于始终使能;
4.若需要使用PWM调速时,需要拔起跳线帽,将能量端接到单片机IO口。(定时器IO口,PWM输出模式);
5.逻辑输入四个端口IN1、IN2、IN3、IN四个接单片机IO一路电机由每两个端口控制。
原因:(1) 由于电机的特点,电机在堵塞或高负荷下会增加电流,可能会影响单片机。(2)新手玩单片机时可能会短路,容易在板上冒烟;
2、0.96寸OLED(I2C通讯)
(1)目前市场主要分为OLED与LCD这2种屏幕;
(2)OLED自发光特性,LCD都是背光,而且OLED不需要,因为它是自发光的。同样的显示,OLED效果更好;
(3)多种接口方式:6800、8080两种并行接口方式,4线穿行SPI接口,IIC接口方式(2线);
(4)不要接过高压,3.3V能正常工作;
(5)OLED缺点是做大后成本高。
本实验采用0.96寸OLED屏幕(通信方式IIC),4个接线柱(SCL,SDA,GND,VCC); IIC通信实现方式: IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 连接微控制器及其外围设备的公司开发的两线串行总线。由数据线组成 SDA 和时钟 SCL 串行总线可以发送和接收数据。由数据线组成 SDA 和时钟 SCL 串行总线可以发送和接收数据。 IIC 总线一般可达 400kbps 以上。
I2C 它支持多从机,即一个 I2C 多个可以挂在控制器下 I2C 不同的设备 I2C设备有不同的设备地址 I2C 可通过主控制器 I2C 访问指定设备的设备地址 I2C设备了。SDA 和SCL 这两条线必须连接一个上拉电阻,通常是 4.7K。其余的 I2C 从设备挂接到 SDA 和 SCL 这两条线可以通过 SDA 和 SCL 多次访问这两条线 I2C设备。
一、开始信号。
2)、发送 I2C 设备地址,每个 I2C 所有设备都有设备地址,通过发送特定的设备地址来确定
定访问哪个 I2C 设备。这是一个 8 高位数据 7 位置是设备地址,最后 1 位是读写位,为
1 这意味着这是一个阅读操作,为了 0 这意味着这是一个写作操作。
3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写作操作,为 1 表示读操作。
4),从机发送 ACK 应答信号。
5)重新发送开始信号。
6)发送要写入数据的寄存器地址。
7),从机发送 ACK 应答信号。
8)发送要写入寄存器的数据。
9),从机发送 ACK 应答信号。
十、停止信号。
I2C 单字节读时序比写时序复杂一点,读时序分为 4 第一步是发送设备地址,第二步是发送要读取的寄存器地址,第三步是重新发送设备地址,最后一步是 I2C 从器件输出要读取的寄存器值,我们具体来看一下这几步。
1)、主机发送起始信号。
2)、主机发送要读取的 I2C 从设备地址。
3)、读写控制位,因为是向 I2C 从设备发送数据,因此是写信号。
4)、从机发送的 ACK 应答信号。
5)、重新发送 START 信号。
6)、主机发送要读取的寄存器地址。
7)、从机发送的 ACK 应答信号。
8)、重新发送 START 信号。
9)、重新发送要读取的 I2C 从设备地址。
10)、读写控制位,这里是读信号,表示接下来是从 I2C 从设备里面读取数据。
11)、从机发送的 ACK 应答信号。
12)、从 I2C 器件里面读取到的数据。
13)、主机发出 NO ACK 信号,表示读取完成,不需要从机再发送 ACK 信号了。
14)、主机发出 STOP 信号,停止 I2C 通信。
3、JDY-31蓝牙模块
市场上蓝牙模块有很多,常见的JDY-xx,HC-xx等系列。其实看似高级的蓝牙功能背后就是简单的
USART 的全称是 Universal Synchronous/Asynchronous Receiver/Transmitter,也就是同步/异步串行收发器。相比 UART 多了一个同步的功能,在硬件上体现出来的就是多了一条时钟线。一般 USART 是可以作为 UART 使用的,也就是不使用其同步的功能。
数据包:串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备得RXD接口,在协议层中规定了数据包的内容,具体包括起始位、主体数据(8位或9位)、校验位以及停止位,通讯的双方必须将数据包的格式约定一致才能正常收发数据。
具体如图所示:
波特率:由于异步通信中没有时钟信号,所以接收双方要约定好波特率,即每秒传输的码元个数,以便对信号进行解码,常见的波特率有4800、9600、115200等。STM32中波特率的设置通过串口初始化结构体来实现。
注意:MCU设置的波特率大小要与蓝牙APP设置的大小一致!
市面上电机有很多,常用的有步进电机,直流减速电机,伺服电机等等; 编码器:用来测量电机转速的仪器元件,常见的有:霍尔编码器,光电编码器等 电机的驱动原理很简单,给电压差即可使得电机转动,调速则利用PWM调节发。
注意:通过判断A与B相哪一位在前,即可判断出正转还是反转
二、CubexMX设置
使用的MCU为stm32f103c8t6:
RCC:
SYS:
注意:Debug这里一定要设置成Serial Wire否则可能出现芯片自锁
GPIO设置:
定时TIM2用来测速与测量正转反转(计数器模式)
定时3:PWM调节
I2C:
USART1:
之后按照自己习惯生成初始化文件
三、代码
自动生成的:
需要自己编写的:
I2C代码:
#include "oled.h"
#include "asc.h"
#include "main.h"
void WriteCmd(unsigned char I2C_Command)//???
{
HAL_I2C_Mem_Write(&hi2c2,OLED0561_ADD,COM,I2C_MEMADD_SIZE_8BIT,&I2C_Command,1,100);
}
void WriteDat(unsigned char I2C_Data)//???
{
HAL_I2C_Mem_Write(&hi2c2,OLED0561_ADD,DAT,I2C_MEMADD_SIZE_8BIT,&I2C_Data,1,100);
}
void OLED_Init(void)
{
HAL_Delay(100); //????????
WriteCmd(0xAE); //display off
WriteCmd(0x20); //Set Memory Addressing Mode
WriteCmd(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
WriteCmd(0xb0); //Set Page Start Address for Page Addressing Mode,0-7
WriteCmd(0xc8); //Set COM Output Scan Direction
WriteCmd(0x00); //---set low column address
WriteCmd(0x10); //---set high column address
WriteCmd(0x40); //--set start line address
WriteCmd(0x81); //--set contrast control register
WriteCmd(0xff); //???? 0x00~0xff
WriteCmd(0xa1); //--set segment re-map 0 to 127
WriteCmd(0xa6); //--set normal display
WriteCmd(0xa8); //--set multiplex ratio(1 to 64)
WriteCmd(0x3F); //
WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
WriteCmd(0xd3); //-set display offset
WriteCmd(0x00); //-not offset
WriteCmd(0xd5); //--set display clock divide ratio/oscillator frequency
WriteCmd(0xf0); //--set divide ratio
WriteCmd(0xd9); //--set pre-charge period
WriteCmd(0x22); //
WriteCmd(0xda); //--set com pins hardware configuration
WriteCmd(0x12);
WriteCmd(0xdb); //--set vcomh
WriteCmd(0x20); //0x20,0.77xVcc
WriteCmd(0x8d); //--set DC-DC enable
WriteCmd(0x14); //
WriteCmd(0xaf); //--turn on oled panel
}
void OLED_SetPos(unsigned char x, unsigned char y) //???????
{
WriteCmd(0xb0+y);
WriteCmd(((x&0xf0)>>4)|0x10);
WriteCmd((x&0x0f)|0x01);
}
void OLED_Fill(unsigned char fill_Data)//????
{
unsigned char m,n;
for(m=0;m<8;m++)
{
WriteCmd(0xb0+m); //page0-page1
WriteCmd(0x00); //low column start address
WriteCmd(0x10); //high column start address
for(n=0;n<128;n++)
{
WriteDat(fill_Data);
}
}
}
void OLED_CLS(void)//??
{
OLED_Fill(0x00);
}
void OLED_ON(void)
{
WriteCmd(0X8D); //?????
WriteCmd(0X14); //?????
WriteCmd(0XAF); //OLED??
}
void OLED_OFF(void)
{
WriteCmd(0X8D); //?????
WriteCmd(0X10); //?????
WriteCmd(0XAE); //OLED??
}
// Parameters : x,y -- ?????(x:0~127, y:0~7); ch[] -- ???????; TextSize -- ????(1:6*8 ; 2:8*16)
// Description : ??codetab.h??ASCII??,?6*8?8*16???
void OLED_ShowStr(unsigned char x, unsigned char y, unsigned char ch[], unsigned char TextSize)
{
unsigned char c = 0,i = 0,j = 0;
switch(TextSize)
{
case 1:
{
while(ch[j] != '\0')
{
c = ch[j] - 32;
if(x > 126)
{
x = 0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<6;i++)
WriteDat(F6x8[c][i]);
x += 6;
j++;
}
}break;
case 2:
{
while(ch[j] != '\0')
{
c = ch[j] - 32;
if(x > 120)
{
x = 0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<8;i++)
WriteDat(F8X16[c*16+i]);
OLED_SetPos(x,y+1);
for(i=0;i<8;i++)
WriteDat(F8X16[c*16+i+8]);
x += 8;
j++;
}
}break;
}
}
// Parameters : x,y -- ?????(x:0~127, y:0~7); N:???.h????
// Description : ??ASCII_8x16.h????,16*16??
void OLED_ShowCN(unsigned char x, unsigned char y, unsigned char N)
{
unsigned char wm=0;
unsigned int adder=32*N;
OLED_SetPos(x , y);
for(wm = 0;wm < 16;wm++)
{
WriteDat(F16x16[adder]);
adder += 1;
}
OLED_SetPos(x,y + 1);
for(wm = 0;wm < 16;wm++)
{
WriteDat(F16x16[adder]);
adder += 1;
}
}
// ????????????????,????????“??——???——????”??????ascll.h?????(????)
//???????:x:?????
// y:???(??0-7)
// begin:????????????????ascll.c???????
// num:????????
// ????“??”,??????????????????0,1,???0,??????,??:x:0,y:2,begin:0,num:2
void OLED_ShowCN_STR(u8 x , u8 y , u8 begin , u8 num)
{
u8 i;
for(i=0;i<num;i++){OLED_ShowCN(i*16+x,y,i+begin);} //OLED????
}
// Parameters : x0,y0 -- ?????(x0:0~127, y0:0~7); x1,y1 -- ?????(???)???(x1:1~128,y1:1~8)
// Description : ??BMP??
void OLED_DrawBMP(unsigned char x0,unsigned char y0,unsigned char x1,unsigned char y1,unsigned char BMP[])
{
unsigned int j=0;
unsigned char x,y;
if(y1%8==0)
y = y1/8;
else
y = y1/8 + 1;
for(y=y0;y<y1;y++)
{
OLED_SetPos(x0,y);
for(x=x0;x<x1;x++)
{
WriteDat(BMP[j++]);
}
}
}
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 Char_Size)
{
unsigned char c=0,i=0;
c=chr-' ';//???????
if(x>128-1){x=0;y=y+2;}
if(Char_Size ==16)
{
OLED_SetPos(x,y);
for(i=0;i<8;i++)
WriteDat(F8X16[c*16+i]);
OLED_SetPos(x,y+1);
for(i=0;i<8;i++)
WriteDat(F8X16[c*16+i+8]);
}
else {
OLED_SetPos(x,y);
for(i=0;i<6;i++)
WriteDat(F6x8[c][i]);
}
}
u32 oled_pow(u8 m,u8 n)
{
u32 result=1;
while(n--)result*=m;
return result;
}
//??2???
//x,y :????
//len :?????
//size:????
//mode:?? 0,????;1,????
//num:??(0~4294967295);
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size2)
{
u8 t,temp;
u8 enshow=0;
for(t=0;t<len;t++)
{
temp=(num/oled_pow(10,len-t-1))%10;
if(enshow==0&&t<(len-1))
{
if(temp==0)
{
OLED_ShowChar(x+(size2/2)*t,y,' ',size2);
continue;
}else enshow=1;
}
OLED_ShowChar(x+(size2/2)*t,y,temp+'0',size2);
}
}
UART代码:
#include "uart.h"
uint8_t USART1_RX_BUF[USART1_REC_LEN];//????,??USART_REC_LEN???.
uint16_t USART1_RX_STA=0;//??????//bit15:??????,bit14~0:??????????
uint8_t USART1_NewData;//?????????1????????
extern int flag;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//????????
{
if(huart ==&huart1)
{
if((USART1_RX_STA&0x8000)==0)//?????
{
if(USART1_NewData==0x5A)//????0x5A
{
USART1_RX_STA|=0x8000; //?????,?USART2_RX_STA??bit15(15?)?1
}
else
{
USART1_RX_BUF[USART1_RX_STA&0X7FFF]=USART1_NewData;
if(USART1_RX_BUF[1] == 0x01)
{
flag = 2;
}
USART1_RX_STA++; //???????1
if(USART1_RX_STA>(USART1_REC_LEN-1))USART1_RX_STA=0;//??????,??????
}
}
HAL_UART_Receive_IT(&huart1,(uint8_t *)&USART1_NewData,1);
}
}
常规的编写如上,但是本人的MCU存在问题,单片机并未接收到预设的数据。
所以,本人项目中采用了下方代码:
#include "uart.h"
uint8_t USART1_RX_BUF[USART1_REC_LEN];//????,??USART_REC_LEN???.
uint16_t USART1_RX_STA=0;//??????//bit15:??????,bit14~0:??????????
uint8_t USART1_NewData;//?????????1????????
extern int flag;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//????????
{
if(huart ==&huart1)
{
USART1_RX_BUF[USART1_RX_STA&0X7FFF]=USART1_NewData;
USART1_RX_STA++; //???????1
if(USART1_RX_STA>(USART1_REC_LEN-1))USART1_RX_STA=0;//??????,??????
if(USART1_RX_BUF[USART1_RX_STA-4] == 0xA0)
{
flag = 1;
}
if(USART1_RX_BUF[USART1_RX_STA-4] == 0x90)
{
flag = 2;
}
if(USART1_RX_BUF[USART1_RX_STA-4] == 0xD0)
{
flag = 3;
}
if(USART1_RX_BUF[USART1_RX_STA-4] == 0x88)
{
flag = 4;
}
if(USART1_RX_BUF[USART1_RX_STA-4] == 0x48)
{
flag = 5;
}
HAL_UART_Receive_IT(&huart1,(uint8_t *)&USART1_NewData,1);
}
}
如果大家自己使用的花,可以根据自己的蓝牙APP写这段程序,有问题欢迎留言
Motor代码:
#include "motor.h"
void MOTOR_GO()
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1,3000);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET);
}
void MOTOR_BACK()
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);
}
void MOTOR_STOP()
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);
}
void MOTOR_UP()
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1,1);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET);
}
void MOTOR_DOWN()
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1,400);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET);
}
PID:
PID算法:
PID分为位置型和增量型
增量型即通过 u(k)-u(k-1) 从而得出式子:
公式的第一部分是比例式 是为了让值按一定比例达到目标值;
第二部分是积分值,正值,在计算的过程中往往会受到环境等一些其他因素的影响,导致值不能到达目标值;
第三部分是微分值,通常是负值,后一次偏差值往往小于前一次偏差值,目的是为了防止值增加过大,通常起一个阻碍的作用;
PID代码:
#include "pid.h"
#include "tim.h"
#include "main.h"
#include "math.h"
#include "i2c.h"
#include "oled.h"
unsigned int MotorSpeed; //È«¾Ö±äÁ¿£¬µç»úµ±Ç°×ªËÙ
int SpeedTarget = 750; //Ä¿±êתËÙ
int MotorOutput; //µç»úÊä³ö
//1.ÀûÓÃTIM2¼ÆËãµç»úתËÙ
void GetMotorSpeed(void)
{
// int CaptureNumber = (short)__HAL_TIM_GET_COUNTER(&htim2); //HAL¿âº¯Êý¼ÆËãÂö³å´ÎÊý
//
// //µç»úתËÙת»»Speed=1sÄÚµÄÂö³åÊý/44(һȦ11¸öÐźţ¬4±¶Æµ·¨)/34¼õËÙ±È
// int MotorSpeed=CaptureNumber*20/44/34*2*3.14*3;
// OLED_ShowNum(40,0,MotorSpeed,4,16);
//
// __HAL_TIM_GET_COUNTER(&htim2) = 0; //¼ÆÊýÆ÷ÇåÁã
int CaptureNumber = (short)__HAL_TIM_GET_COUNTER(&htim2); //???????
__HAL_TIM_GET_COUNTER(&htim2) = 0;
// int Speed=CaptureNumber*5/44/34*2*3.14*3;
int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2);
if(Direction == 1)
{
CaptureNumber -= 65535;
}
MotorSpeed=CaptureNumber;
OLED_ShowNum(40,0,MotorSpeed,4,16);
HAL_Delay(100);
OLED_CLS();
// __HAL_TIM_GET_COUNTER(&htim2) = 0;
}
//2.ÔöÁ¿Ê½PID¿ØÖÆÆ÷£¨PID³£¼û·ÖΪλÖÃPIDºÍÔöÁ¿Ê½PID£©
int Error_Last,Error_Prev; //ÉÏ´ÎÎó²î£¬ÉÏÉÏ´ÎÎó²î
int Pwm_add,Pwm; //PWMÔöÁ¿,PWMÕ¼¿Õ±È
int Kp = 5, Ki = 3, Kd = 1;//PIDË㷨ϵÊý£¬¸¡µãÀàÐÍ£¬Ð¾Æ¬¼ÆËãÄÜÁ¦Ò»°ãʱ½¨ÒéÕûÐÍ£¬»òÕß*1024
int SpeedInnerControl(int Speed,int Target) //ËÙ¶ÈÄÚ»·¿ØÖÆ
{
int Error = Target - Speed; //Îó²î = Ä¿±êËÙ¶È - ʵ¼ÊËÙ¶È
Pwm_add = Kp * (Error - Error_Last) + //±ÈÀý
Ki * Error + //»ý·Ö
Kd * (Error - 2.0f * Error_Last + Error_Prev); //΢·Ö
Pwm += Pwm_add; //Êä³öÁ¿=ÔʼÁ¿+ÔöÁ¿
Error_Prev = Error_Last; //±£´æÉÏÉÏ´ÎÎó²î
Error_Last = Error; //±£´æÉÏ´ÎÎó²î
if(Pwm > 4999) Pwm = 3000; //ÏÞÖÆÉÏÏÂÏÞ£¬·ÀÖ¹PWM³¬³öÁ¿³Ì
if(Pwm <-4999) Pwm =-3000;
return Pwm; //·µ»ØÊä³öÖµ
}
//3.µç»úתËÙÓë·½ÏòµÄº¯Êý£¨PID¿ØÖÆ£©
void SetMotorVoltageAndDirection(int Pwm)
{
if(Pwm < 0) //Èç¹ûPWMСÓÚ0
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);
Pwm = (-Pwm); //PWMÖ»ÄÜÈ¡ÕýÖµ£¬Èç¹ûΪ¸ºÊý£¬Ö±½ÓÈ¡·´
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, Pwm); //PWMµ÷ËÙ
} else
{
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, Pwm); //PWMµ÷ËÙ
}
}
void ModePID()
{
GetMotorSpeed();
MotorOutput = SpeedInnerControl(MotorSpeed,SpeedTarget);
SetMotorVoltageAndDirection(MotorOutput);
}
主函数代码:
while (1)
{
switch(flag)
{
case(1):MOTOR_GO();break;
case(2):MOTOR_BACK();break;
case(3):MOTOR_STOP();break;
case(4):MOTOR_UP();break;
case(5):ModePID();break;
default:break;
}
/* USER CODE END WHILE */
if(flag != 5)
{
int CaptureNumber = (short)__HAL_TIM_GET_COUNTER(&htim2); //???????
__HAL_TIM_GET_COUNTER(&htim2) = 0;
// int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2);
//µç»úתËÙת»»Speed=1sÄÚµÄÂö³åÊý/44(һȦ11¸öÐźţ¬4±¶Æµ·¨)/34¼õËÙ±È
// int Speed=CaptureNumber*5/44/34*2*3.14*3;
int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2);
if(Direction == 1)
{
CaptureNumber -= 65535;
}
int Speed=CaptureNumber;
OLED_ShowNum(40,0,Speed,5,16);
HAL_Delay(100);
OLED_CLS();
}
int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2);
OLED_ShowCN_STR(0,0,0,3);
// OLED_ShowNum(40,0,Speed,4,16);
OLED_ShowStr(90,0,"cm/s",2);
OLED_ShowCN_STR(0,3,3,2);
if(Direction==0)
{
OLED_ShowCN_STR(40,3,5,2);
}
if(Direction==1)
{
OLED_ShowCN_STR(40,3,7,2);
}
// HAL_Delay(1000);
// OLED_CLS();
/* USER CODE BEGIN 3 */
}
蓝牙APP源代码以及技术论文:链接:https://pan.baidu.com/s/1-rbicxuyLVCq6rglCWcJTg 提取码:huzm