一 第一个模块是Servo(舵机模块):
1 Init函数:
先是开启GPIOB和TIM时基单元的时钟分别是APB2和APB1
配置输出端口:然后配置输出端口PB1为复用推挽输出模式,设置引脚的翻转速度为50MHZ,最后
初始化GPIO端口。
(② PB0 设为复用推挽 —— 引脚交给 TIM3 控制,不再归 GPIO 管
c GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_AF_PP; // AF = 复用(交给外设),PP = 推挽(能推能拉) gpio.GPIO_Speed = GPIO_Speed_50MHz; // 翻转速度设最高,PWM 边沿才陡 GPIO_Init(GPIOB, &gpio);
)
配置时基单元:配置TIM3的时基单元,然后先定义一个时基单元的结构体,然后配置这个时基单
的预分频器(计数频率=系统时钟频率/(预分频器+1)),然后配置自动重装载值为20000-1
(因为PWM要求的频率为50HZ,周期为20ms),然后配置时钟分频因子(滤波器的采样频率),
然后配置计数器为向上计数,最后初始化时基单元,讲结构体写到寄存器即可。
// ③ 配定时器时基 —— 决定 PWM 周期
c TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler = 72 - 1; // 写 71 = 除以 72 → 1MHz tim.TIM_Period = 20000 - 1; // 写 19999 = 计 20000 个数 → 20ms tim.TIM_ClockDivision = 0; // 不分频 tim.TIM_CounterMode = TIM_CounterMode_Up; // 0→ARR→归零,循环 TIM_TimeBaseInit(TIM3, &tim);
配置输出通道:先定义一个输出比较结构体,然后先初始化结构体让结构体的成员为默认值,然
后PWM的模式为PWM1(当计数器值小于Pulse时输出有效电平,大于Pulse时输出无效电平,PWM2
模式则是反过来),然后通道使能输出让PB0输出波形,然后配置比较值Pulse,然后配置有效电
平为高电平或者低电平,最后将这个结构体写入寄存器。
// ④ 配输出通道 —— 决定 PWM 占空比
c TIM_OCInitTypeDef oc; TIM_OCStructInit(&oc); // 先全部清零,防止未初始化的字段是随机值 oc.TIM_OCMode = TIM_OCMode_PWM1; // PWM1: 计数 < Pulse → 高,> Pulse → 低 oc.TIM_OutputState = TIM_OutputState_Enable; // 使能输出 oc.TIM_Pulse = 1500; // 初始 1.5ms = 90° 中立位 oc.TIM_OCPolarity = TIM_OCPolarity_High; // 高电平有效 TIM_OC3Init(TIM3, &oc); // 通道 3,对应 PB0
使能输出:
// ⑤ 开定时器 —— 至此 PWM 开始输出
TIM_Cmd(TIM3, ENABLE);
2 Servo_SetAngle函数:
根据Pulse=500等效于角度0,Pulse=2500等效于角度1800,根据传入的角度线性转化为对应的
Pulse,然后写入比较寄存器。
void Servo_SetAngle(uint8_t angle)
{
if (angle > 180) angle = 180; // 边界保护
uint16_t pulse = 500 + (uint32_t)angle * 2000 / 180; // 角度 → 脉宽
TIM_SetCompare3(TIM3, pulse); // 写入比较寄存器
}
二 第二个模块是Bluetooth模块: 1 Init函数: 先开启USART和GPIOA外设的时钟,接着配置引脚PA9为复用推挽输出,然后配置PA10为浮空输入。 接着配置USART1,先定义一个USART的结构体,然后配置波特率(每秒传输多少位数据)为9600 ,然后配置一帧数据位的长度位8位或9位,然后配置停止位的长度为1(也有其它位的),接着配置 奇偶校验位无(也可以奇校验或偶校验),接着配置硬件流控为无,然后配置串口的工作模式为 接收和发送,然后将这个结构体写入寄存器中,然后配置中断NVIc,先定义一个中断结构体,然后 配置中断通道为USART1中断通道号,然后配置抢占优先级和响应优先级,最后使能这条中断通道, 然后将该结构体写到寄存器中,我的理解的配置完中断NVIC以后USART1的中断信号就可以经过NVIC 传到CPU了,最后配置USART1的ERXNE中断使能(这个是使USART1发出中断信号),最后再启动USART1. ```c RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin=GPIO_Pin_9;
gpio.GPIO_Mode=GPIO_Mode_AF_PP;
gpio.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&gpio);
gpio.GPIO_Pin = GPIO_Pin_10; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 = 只读电平,不拉高也不拉低 GPIO_Init(GPIOA, &gpio);
// ④ 配 USART1 参数 USART_InitTypeDef uart; uart.USART_BaudRate = 9600; // 波特率,HC-05 默认 9600 uart.USART_WordLength = USART_WordLength_8b; // 8 数据位 uart.USART_StopBits = USART_StopBits_1; // 1 停止位 uart.USART_Parity = USART_Parity_No; // 无校验 uart.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控 uart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 既要收也要发 USART_Init(USART1, &uart);
// ⑤ 开接收中断 — NVIC 配置 NVIC_InitTypeDef nvic; nvic.NVIC_IRQChannel = USART1_IRQn; // USART1 中断通道号 nvic.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级(数字越小越优先) nvic.NVIC_IRQChannelSubPriority = 1; // 响应优先级 nvic.NVIC_IRQChannelCmd = ENABLE; // 使能这条中断通道 NVIC_Init(&nvic);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 开 RXNE 中断:收到一字节才触发 USART_Cmd(USART1, ENABLE); // 启动 USART1
2 void BT_SendStr函数:
先判断那个传输进来的字符指针是不是空,如果不是就进入循环体内部,然后通过一个函数来判断发送数据寄存器空的标志位
是否位RESETUSART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET,如果是RESET那么代表发送数据寄存器不是空就
代表还有数据要发送,然后通过USART_SendData(USART1, *str++);发送数据,这个函数是先发送了,再自增加。
```c
void BT_SendStr(const char *str)
{
while (*str)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等上一个字节发完
USART_SendData(USART1, *str++); // 发当前字节,指针后移
}
}
```
3 void BT_SendNum函数:
串口只认识ASCII码,所以需要把数字转化为字符才能通过串口发送。
```c
void BT_SendNum(int32_t num)
{
char buf[12];
sprintf(buf, "%d", num); // sprintf 把整数格式化成字符串
BT_SendStr(buf); // 复用上面的发字符串函数
}
```
4 uint8_t BT_GetCmd函数:
通过全局变量static volatile uint8_t bt_rx_flag = 0;判断有没有数据发送过来,如果有就返回这个数据。
```c
uint8_t BT_GetCmd(void)
{
if (bt_rx_flag == 0) return 0; // 没新数据
bt_rx_flag = 0; // 清标志
return bt_rx_byte; // 返回收到的字节
}
```
5 void USART1_IRQHandler函数:
先通过函数USART_GetITStatus(USART1, USART_IT_RXNE) == SET来判断中断是由接收字节产生的,然后通过
函数USART_ClearITPendingBit(USART1, USART_IT_RXNE);清楚中断标志位,然后通过函数USART_ReceiveData(USART1)
来获取数据保存到全局变量bt_rx_byte里,再将全局变量bt_rx_flag设置位1就行。
```c
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) // 确实是接收中断
{
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清中断标志(必须清!)
bt_rx_byte = USART_ReceiveData(USART1); // 读收到的字节
bt_rx_flag = 1; // 置有新数据标志
}
}
```
三 第三个模块是DHT11模块(温湿度传感器模块)
预先在.h文件里面定义五个宏:
```c
#define DHT11_OUT() { GPIO_InitTypeDef g; g.GPIO_Pin=DHT11_PIN; g.GPIO_Mode=GPIO_Mode_Out_OD; g.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT,&g); }
#define DHT11_IN() { GPIO_InitTypeDef g; g.GPIO_Pin=DHT11_PIN; g.GPIO_Mode=GPIO_Mode_IPU; GPIO_Init(DHT11_PORT,&g); }
#define DHT11_HIGH() GPIO_SetBits(DHT11_PORT, DHT11_PIN)
#define DHT11_LOW() GPIO_ResetBits(DHT11_PORT, DHT11_PIN)
#define DHT11_RD() GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN)
(1)为什么用宏而不是函数呢,因为函数需要先传递参数到栈然后跳转到函数地址然后读引脚寄存器 然后把值传到寄存器,然后跳回到调用处然后将参数出栈,而宏直接读引脚寄存器直接执行寄存器操作,会更快。 (2)还有要切换为输出模式才能操作输出寄存器,输出模式下,0是将引脚接到GND,1是释放引脚,然后电流会通过上拉电 阻不至于短路。 (3)学到了移位操作val <<= 1是将最左边的一位移出去然后最右边腾出一个0,还有或操作val |= 1与右边的数字做 比较,如果两个数对应的位值有一个1,那么新数该位就是1. (4)static前缀修饰的函数代表了把这个函数锁在了这个.c文件内,其它文件看不见,这样就防止了函数乱调用。 1 void DHT11_Init函数: 先开启GPIOA的时钟,然后配置为输出模式再将输出寄存器设置为高电平告诉DHT11总线是空闲的。
void DHT11_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
DHT11_OUT();
DHT11_HIGH(); // 空闲拉高
}
2 DHT11_Read函数: 这个函数是读取DHT11的数据,温度整数和小数,以及湿度的整数和小数还有校验和。主机先拉低20ms再拉高30us, 然后DHT11先拉低80us再拉高80us表示确认应答以后开始向主机发送数据,主机接收到数据以后将每个小数整数加起来 与校验和做比较,如果相等则代表数据正确,返回0,如果不相等则说明数据错误返回2,如果没应答则返回1,同时将 输出寄存器设为1,将引脚设置为高电平,也就是高阻态告诉DHT11现在总线是空闲的
uint8_t DHT11_Read(DHT11_Data *data)
{
uint8_t buf[5];
// ① 主机发起始信号:拉低 20ms → 拉高 30us
DHT11_OUT();
DHT11_LOW();
Delay_ms(20);
DHT11_HIGH();
Delay_us(30);
// ② 等 DHT11 应答:拉低 80us → 拉高 80us
DHT11_IN();
Delay_us(40);
if (DHT11_RD() == 0) // DHT11 拉低了 = 有应答
{
Delay_us(80);
if (DHT11_RD() == 1) // DHT11 拉高了 = 确认应答
{
Delay_us(40);
// ③ 连读 5 字节
buf[0] = ReadByte(); // 湿度整数
buf[1] = ReadByte(); // 湿度小数
buf[2] = ReadByte(); // 温度整数
buf[3] = ReadByte(); // 温度小数
buf[4] = ReadByte(); // 校验和
DHT11_OUT();
DHT11_HIGH(); // 释放总线
// ④ 校验
if (buf[0] + buf[1] + buf[2] + buf[3] != buf[4])
return 2;
data->humi_int = buf[0];
data->humi_deci = buf[1];
data->temp_int = buf[2];
data->temp_deci = buf[3];
return 0;
}
}
DHT11_OUT();
DHT11_HIGH();
return 1;
}
3 static uint8_t ReadByte(void)函数: 这个函数是收集DHT11发来的数据,先定义一个val=0,然后循环八次收集数据,然后将用val<<=1让这个数左移一位。 在这个循环里再设置一个循环忙等,如果是低电平则一直等待,变成高电平以后delay忙等30us,如果30us以后还是 高电平则该位是1然后通过val|=1来将val的最低为变成1,否则是0就不用操作。然后等待高电平结束。
三 第三个模块是hscr04模块(超声波测距离) 1 HCSR04_Init函数 我的理解是trig是hscr04的输入引脚echo是hsrc04的输出引脚,一开始先配置PA0为推挽输出,PA1为浮空输入,然后我门配置stm32的PA0为推挽输出,PA1为浮空输入,然后配置时基单元觉得TIM2的计数模式, 然后配置输入捕获,检测引脚PA1的高低电平的变化瞬间,先根据stm32的输入引脚决定了选择的通道,然后选择是上升沿捕获还是下降沿,然后确定是直接来自输入引脚的检查而不是其它通道映射过来的,然后确定每一次边界沿都触发一次,然后确定每次采样八次都是同一个电平才确定电平变化,然后将这个输入捕获写入寄存器,然后开启边沿变化中断和溢出中断, 然后配置NVIC先确定中断信号来自TIM2,再配置优先级,最后使能这个NVIC中断,最后启动时基单元。
void HCSR04_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
GPIO_InitTypeDef g;
g.GPIO_Pin = TRIG_PIN;
g.GPIO_Mode = GPIO_Mode_Out_PP;
g.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(TRIG_PORT, &g);
GPIO_ResetBits(TRIG_PORT, TRIG_PIN);
g.GPIO_Pin = ECHO_PIN;
g.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(ECHO_PORT, &g);
TIM_TimeBaseInitTypeDef t;
t.TIM_Prescaler = 72 - 1;
t.TIM_Period = 0xFFFF;
t.TIM_ClockDivision = 0;
t.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &t);
TIM_ICInitTypeDef ic;
ic.TIM_Channel = TIM_Channel_2;
ic.TIM_ICPolarity = TIM_ICPolarity_Rising;
ic.TIM_ICSelection = TIM_ICSelection_DirectTI;
ic.TIM_ICPrescaler = TIM_ICPSC_DIV1;
ic.TIM_ICFilter = 0x0F;
TIM_ICInit(TIM2, &ic);
TIM_ITConfig(TIM2, TIM_IT_CC2 | TIM_IT_Update, ENABLE);
NVIC_InitTypeDef n;
n.NVIC_IRQChannel = TIM2_IRQn;
n.NVIC_IRQChannelPreemptionPriority = 1;
n.NVIC_IRQChannelSubPriority = 1;
n.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&n);
TIM_Cmd(TIM2, ENABLE);
}
2 HCSR04_GetDist stm32测距前先向hscr发送15us的高脉冲告诉hscr04开始测距,然后hscr04发送超声波以后 先将PA0变回低电平,然后echo自动拉高PA1电平,高电平持续时间便是超声波飞行时间,然后 进入循环知道超声波回来,如果过了这个循环还不回来则测距失败,回来则返回计算的距离值。
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_CC2))
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC2);
if (GPIO_ReadInputDataBit(ECHO_PORT, ECHO_PIN))
{
TIM_SetCounter(TIM2, 0);
TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Falling);
}
else
{
echo_val = TIM_GetCapture2(TIM2);
echo_ok = 1;
TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Rising);
}
}
if (TIM_GetITStatus(TIM2, TIM_IT_Update))
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
3 TIM2_IRQHandler
先判断是哪个定时器的哪个中断源,因为一个定时器有多个中断源但只有一个中断入口,我们需要
直接查询是哪个中断源照成的中断,先判断是CH2捕获照成的还是计数器溢出照成的,如果是CH2捕获
照成的则先清楚标志位然后判断输入寄存器的状态是高电平还是低电平,如果是高电平则说明刚刚是上升沿,则说明c
超声波刚发出,于是将计数器记为0同时将CH2捕获下一次会因为是下降沿而触发,如果是低电平则说明
刚刚是下降沿,说明超声波已经回到hscr04,那么记录此时计数器的值,将echo_OK设为1,同时将CH2捕获下一次会因为上升
沿触发。如果中断是计数器溢出中断源照成,那么便将该标志位清楚即可,因为溢出以后代表本次测量失败,硬件会自动将
计数器设为0,我们只需要清除标志位即可
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_CC2))
{
TIM_ClearITPendingBit(TIM2, TIM_IT_CC2);
if (GPIO_ReadInputDataBit(ECHO_PORT, ECHO_PIN))
{
TIM_SetCounter(TIM2, 0);
TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Falling);
}
else
{
echo_val = TIM_GetCapture2(TIM2);
echo_ok = 1;
TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Rising);
}
}
if (TIM_GetITStatus(TIM2, TIM_IT_Update))
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
四 第四个模块是motor(微型电机模块) 这个模块因为在Servo模块舵机模块配置了TIM3,这个模块与舵机模块共用一个时基单元TIM3,所以不用配置,先配置PB12和PB13为 推挽输出,然后配置PB1为复用推挽输出由TIM3_CH3内置外设控制输出PWM波,复用推挽输出是由cpu控制电平高低,而推挽输出是由 我们用函数来控制,然后接下来配置PWM波,先将PWM设置为模式1(oc.TIM_OCMode=TIM_OCMode_PWM1;),这样小于脉冲输出高电平 ,大于脉冲输出低电平,然后使能通道输出(oc.TIM_OutputState = TIM_OutputState_Enable;)这样才能输出PWM波,然后设置 脉冲为0这样就能输出低电平(oc.TIM_Pulse=0;),舵机不会动,然后设置哪个电平有效(oc.TIM_OCPolarity=TIM_OCPolarity_High;) 我的理解是决定了微型电机在高电平转还是低电平转,然后将AIN1/AIN2引脚全设置为低电平并将占空比设为0,后面的改变转动方向和速度 无非是改变AIN1和AIN2的高低电平,以及PWM的占空比即可,停止舵机便是将AIN1/AIN2设为低电平,并将占空比设为0就好了。
#include "stm32f10x.h"
void Servo_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_0;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
TIM_TimeBaseInitTypeDef tim;
tim.TIM_Prescaler = 72 - 1;
tim.TIM_Period = 20000 - 1;
tim.TIM_ClockDivision = 0;
tim.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &tim);
TIM_OCInitTypeDef oc;
TIM_OCStructInit(&oc);
oc.TIM_OCMode = TIM_OCMode_PWM1;
oc.TIM_OutputState = TIM_OutputState_Enable;
oc.TIM_Pulse = 1500;
oc.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC3Init(TIM3, &oc);
TIM_Cmd(TIM3, ENABLE);
}
void Servo_SetAngle(uint8_t angle)
{
if (angle > 180) angle = 180;
uint16_t pulse = 500 + (uint32_t)angle * 2000 / 180;
TIM_SetCompare3(TIM3, pulse);
}