找回密码
 立即注册
首页 业界区 安全 hal库总结学习

hal库总结学习

艺轫 昨天 21:03
我会在hal库学习中添加一些问题及解答
1.GPIO口输入输出:

1.1推挽输出(GPIO_MODE_OUTPUT_PP):
任务:将小灯点亮

同时存在P-MOS 管和N-MOS 管,两个管子互补工作,源极分别接 VDD(3.3V)和 VSS(0V 地),漏极共同连接到 I/O 引脚。
我们通过代码写入 GPIO 的高低电平 →控制输出数据寄存器控制 P-MOS/N-MOS 的导通 / 截止
P-MOS 导通,N-MOS 截止,脚通过导通的 P-MOS 直接连接到 VDD(3.3V),引脚电平被强制拉高到 3.3V,此时有很强的拉电流能力,电路导通,小灯亮。
那么控制N-MOS 导通,P-MOS 截止, N-MOS 直接连接到地(0V),引脚电平被强制拉低到 0V,小灯灭。
HAL库中控制 P-MOS/N-MOS 的导通 / 截止的函数是啥?
  1. HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);    // 输出高电平(点亮LED)P-MOS导通,N-MOS截止HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);  // 输出低电平(熄灭LED)P-MOS截止,N-MOS导通
复制代码
我们通过程序控制输出数据寄存器从而输出数据控制N-MOS 管的导通 / 截止,但是发现现在小灯亮了但没完全亮(工作电压为5V,但是VDD是3.3V),那如何让小灯在工作电压5V工作呢,这时候需要开漏输出了。
1.2开漏输出(GPIO_MODE_OUTPUT_OD):
开漏输出的本质是 “只能主动拉低,不能主动拉高”
输出逻辑 0,控制 N-MOS 管导通,引脚通过导通的 N-MOS 直接连接到地(0V),引脚电平被强制拉低到 0V
输出逻辑 1,逻辑控制 N-MOS 管截止,引脚与地完全断开,呈现高阻态,芯片不再驱动引脚引脚电平完全由外部电路决定:必须外接上拉电阻到电源,才能将引脚拉到高电平;无上拉时引脚电平不确定(浮空),这时候外接5V电源就可以完全让小灯亮起来了。

如电路图:仅保留了N-MOS 管,P-MOS 管被完全关闭(不参与工作),N-MOS 的源极直接接 VSS(0V 地),漏极连接到 I/O 引脚。(忽略复用输入功能)



1.3复用输出
复用推挽输出(AF_PP):外设驱动推挽结构,高低电平均由外设主动驱动
复用开漏输出(AF_OD):外设驱动开漏结构,仅能主动拉低,高电平需上拉
外设自动控制,软件仅能配置外设,无法直接写引脚,推挽 / 开漏结构不变,仅切换输入源(图中黄色的线)
2输入部分
2.1浮空输入(Floating Input)
上下拉电阻全部断开,引脚完全悬空.
输入阻抗极高,引脚电平完全由外部电路决定,悬空时电平不确定(易受干扰)
一般用于外部中断、按键(需外接上拉 / 下拉电阻)、UART RX 等外部信号输入
  1. GPIO_InitStruct.Mode = GPIO_MODE_INPUT;        // 通用输入GPIO_InitStruct.Pull = GPIO_NOPULL;               // 浮空,无上下拉
复制代码
2.2上拉输入(Pull-up Input)
接通 VDD 侧上拉电阻,断开 VSS 侧下拉电阻
引脚悬空时,被内部上拉电阻拉至高电平(VDD,3.3V);外部拉低时变为低电平
应用:按键输入(按键一端接引脚,一端接地,按下时引脚变低,松开自动上拉为高,无需外接电阻)
  1. GPIO_InitStruct.Mode = GPIO_MODE_INPUT;          // 通用输入模式GPIO_InitStruct.Pull = GPIO_PULLUP;               // 开启上拉
复制代码
2.3下拉输入(Pull-down Input)
配置:接通 VSS 侧下拉电阻,断开 VDD 侧上拉电阻
特性:引脚悬空时,被内部下拉电阻拉至低电平(0V);外部拉高时变为高电平
典型应用:按键输入(按键一端接引脚,一端接 VDD,按下时引脚变高,松开自动下拉为低)、传感器低电平有效信号
  1. GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_PULLDOWN;             // 开启下拉
复制代码

2.4模拟输入:
输出驱动器(P-MOS/N-MOS)完全关闭,引脚与内部数字电路完全隔离,无任何数字驱动
上下拉电阻开关全部断开,引脚呈高阻态,不影响外部模拟信号
TTL 肖特基触发器(数字输入缓冲)被关闭,切断数字信号路径
啥是肖特基?
当外部信号有噪声、抖动、缓慢上升沿,普通比较器(单阈值)在中间电压会疯狂乱跳,肖特基触发器:只有明显高 / 明显低才翻转,中间波动直接吃掉。经过处理的电压呈现方波信号,即数字信号(0,1);所以没处理的是啥——模拟信号。

模拟输入路径直接连通:I/O 引脚 → 模拟输入 → 片上外设(ADC)
2.5复用功能输入:
是指将 GPIO 引脚从 “通用输入” 切换为 “片上外设专用输入”,让外部信号直接送给串口、SPI、定时器、I²C 等硬件模块,而不只是给 CPU 读高低电平,信号去向是外设,硬件模块来用,USART_RX、SPI_MISO、TIM_CHx 输入、I²C_SDA(片上外设)
3.EXTI(外部中断 / 事件控制器)
那复用功能输入的DC信号去哪了呢?
去EXTI了xdm。

有19个GPIO口中有EXTI,所以有输入线19 根,外部信号入口,对应 19 个 EXTI 线:
・EXTI0~EXTI15:GPIO 引脚(PAx~PDx 对应 EXTIx)
・EXTI16:PVD 电源电压检测
・EXTI17:RTC 闹钟
・EXTI18:USB 唤醒(F1 系列)
边沿检测电路检测输入线的电平变化,根据配置触发:
・上升沿触发(RISING)
・下降沿触发(FALLING)
・双边沿触发(BOTH)

他们可以编程调用,所以有对应的寄存器
软件中断事件寄存器通过软件写 1,手动触发中断 / 事件(无需外部信号),常用于调试。
或门(OR Gate)        合并「硬件边沿触发」和「软件触发」的信号,只要任意一个有效,就产生请求。
请求挂起寄存器:锁存中断请求:当有触发信号时,对应位被置 1,直到 CPU 响应中断后硬件自动清零
中断屏蔽寄存器        控制「中断请求」是否发送给 NVIC:
置 1:允许中断,请求会发送到 NVIC,置 0:屏蔽中断,请求被拦截(cubemx中只要将GPIO的NVIC打开自动就挂起1)
仅当「请求挂起」和「事件屏蔽」同时有效时,才输出事件信号

NVIC(中断控制器):接收中断请求,进行优先级仲裁,最终触发 CPU 进入中断服务函数(把他看作内部存着一个表的内存,里面存着中断函数)
中断服务函数名中断向量号对应管理的 EXTI 线说明EXTI0_IRQHandlerEXTI0_IRQnEXTI0独立中断向量,仅对应 EXTI0EXTI1_IRQHandlerEXTI1_IRQnEXTI1独立中断向量,仅对应 EXTI1EXTI2_IRQHandlerEXTI2_IRQnEXTI2独立中断向量,仅对应 EXTI2EXTI3_IRQHandlerEXTI3_IRQnEXTI3独立中断向量,仅对应 EXTI3EXTI4_IRQHandlerEXTI4_IRQnEXTI4独立中断向量,仅对应 EXTI4EXTI9_5_IRQHandlerEXTI9_5_IRQnEXTI5、EXTI6、EXTI7、EXTI8、EXTI9共用中断向量,需在函数内判断具体触发线EXTI15_10_IRQHandlerEXTI15_10_IRQnEXTI10、EXTI11、EXTI12、EXTI13、EXTI14、EXTI15共用中断向量,需在函数内判断具体触发线官方HAL库的中断函数定义

具体可以这么用:
  1. void EXTI15_10_IRQHandler(void){  // 依次判断10-15号EXTI线的挂起位  if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_10) != RESET)  {    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_10)//cubemx自动生成清除中断标志位,cubemx好用爱用;    HAL_GPIO_EXTI_Callback(GPIO_PIN_10);  }  if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_11) != RESET)  {    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_11);    HAL_GPIO_EXTI_Callback(GPIO_PIN_11);  }..................................if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_15) != RESET)  {    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_15);    HAL_GPIO_EXTI_Callback(GPIO_PIN_15);  }
复制代码
EXTI 线编号与GPIO引脚一 一对应
EXTI 线编号可绑定的 GPIO 引脚EXTI0PA0、PB0、PC0EXTI1PA1、PB1、PC1EXTI2PA2、PB2、PC2...........easy~
1.STM32F1 RTC 核心特性
时钟源:
推荐使用 LSE(32.768kHz 晶振),计时精度最高
备选 LSI(约 40kHz,精度较差,仅适合临时场景)
HSE 分频不推荐用于 RTC,会导致计时误差大
功能:
日历计时(秒 / 分 / 时 / 日 / 月 / 年)
闹钟(Alarm)触发中断
唤醒(Wakeup)定时器(部分封装支持)
备份寄存器(BKP)存储关键数据,掉电由 VBAT 保持
2.STM32 UART/USART 工作模式详解

1.基础通信模式
Asynchronous(异步模式)
最常用的 UART 模式,无需时钟线,仅 TX/RX 两根线
通信双方约定波特率、数据位、校验位、停止位
适用场景:串口调试、与 PC / 传感器 / 蓝牙模块通信
Synchronous(同步模式)
额外提供 SCLK 时钟线,由主机提供时钟同步数据
数据传输速率更稳定,适合高速短距通信
适用场景:与 SPI 兼容设备、某些显示模块通信
Single Wire (Half-Duplex)(单线半双工)
共用一根线(TX)进行收发,同一时间只能单向传输
节省 IO 口,适合总线式多设备通信
适用场景:RS485 总线、低成本多点通信
2.专用协议模式
Multiprocessor Communication(多处理器通信)
支持地址帧 / 数据帧区分,实现多设备总线寻址
适合一个主机带多个从机的场景
IrDA(红外数据协会模式)
兼容红外通信协议,通过红外收发器实现无线短距传输
适用场景:红外遥控器、旧款便携设备
LIN(本地互连网络模式)
符合 LIN 2.0/2.1 协议,用于汽车车身电子低成本通信
单总线、主从结构,速率最高 20kbps
SmartCard / SmartCard with Card Clock(智能卡模式)
兼容 ISO 7816 智能卡协议,支持卡时钟输出
适用场景:SIM 卡、金融 IC 卡、身份识别卡


  • 基础参数(Basic Parameters)
参数配置值详细说明Baud Rate115200 Bits/s波特率:每秒传输 115200 位数据,是嵌入式开发最常用的速率之一,兼顾速度与稳定性Word Length8 Bits (including Parity)数据位长度:8 位(包含校验位),当前无校验位,实际有效数据为 8 位ParityNone校验位:无校验,不进行数据错误检测,传输效率更高Stop Bits1停止位:1/2 位,用于标识一帧数据的结束3.DMA 完全讲解(结合你的 UART + STM32 场景)
1.什么是 DMA?
DMA = 直接存储器访问一句话:让数据自己搬,不用 CPU 盯着。
普通串口发送 / 接收:CPU 一个字节一个字节发 / 收,全程占用
DMA 模式:CPU 只需要告诉 DMA「从哪搬、搬到哪、搬多少」,然后 CPU 就可以去干别的,搬完 DMA 会通知 CPU
优点:
解放 CPU,不阻塞主循环
高速传输、稳定
串口大量数据收发必备

  • 你现在的场景:UART + DMA
你刚才看的是 UART 模式,现在配 DMA 就是让串口收发不占用 CPU。
UART 常用 3 种 DMA:
TX DMA:串口发送数据用 DMA
RX DMA:串口接收数据用 DMA
RX DMA + 空闲中断 (IDLE):接收不定长数据(最常用、最实用)

  • CubeMX 配置 DMA(一步到位)
① 进入 USARTx → DMA Settings
② Add 添加
USART_TX:选择 Normal 正常模式
USART_RX:选择 Circular 循环模式(适合不停接收)
③ 优先级
一般 Medium 即可
数据量大可以设 High
④ 数据宽度
Byte(串口都是 1 字节)

  • 最重要的 3 个函数(直接复制用)
    1)DMA 发送串口数据(不阻塞)
  1. // 发送 len 个数据,不阻塞 CPUHAL_UART_Transmit_DMA(&huart1, (uint8_t*)buf, len);2)DMA 循环接收(后台一直收)
复制代码
  1. // 开启 DMA 接收,存到 buf,最大接收 100 字节uint8_t uart_buf[100];HAL_UART_Receive_DMA(&huart1, uart_buf, 100);
复制代码
3)DMA 发送完成回调
  1. void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){  if(huart == &huart1)  {    // 发送完成后你要做的事  }}
复制代码
  1. // 接收缓存uint8_t rx_buf[128];uint8_t rx_len = 0;// 初始化时开启HAL_UART_Receive_DMA(&huart1, rx_buf, 128);__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);void USART1_IRQHandler(void){  if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))  {    __HAL_UART_CLEAR_IDLEFLAG(&huart1);    HAL_UART_DMAStop(&huart1);        // 本次收到的数据长度    rx_len = 128 - __HAL_DMA_GET_COUNTER(huart1.hdmarx);        // 在这里处理数据    // ...        // 重新开启 DMA 接收    HAL_UART_Receive_DMA(&huart1, rx_buf, 128);  }  HAL_UART_IRQHandler(&huart1);}
复制代码

4.串口重定向(printf 输出到 UART)
重定向,让 C 语言的 printf / scanf 直接用你的串口(UART)输出,支持 DMA / 阻塞 / 中断 任意模式。

  • 直接可用代码(最常用)
    在你的 main.c 或 usart.c 里添加这段代码:
  1. /* 必须添加这个头文件 *///# include "stdio.h"/* 重定向 fputc:printf 会调用这个函数发送字符 */int fputc(int ch, FILE *f){  /* 你的串口号,比如 huart1 */  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);  return ch;}/* 重定向 fgetc:scanf 会调用这个函数接收字符 */int fgetc(FILE *f){  uint8_t ch = 0;  HAL_UART_Receive(&huart1, &ch, 1, HAL_MAX_DELAY);  return ch;}
复制代码
添加后,直接在代码里写:
printf("Hello 重定向成功!\r\n");
注意在usart.c添加这个typedef struct __FILE FILE;
串口助手就能收到文字!
for example:
  1. uint8_t c_Data[] = "串口输出测试:nb666\r\n";HAL_UART_Transmit(&huart1,c_Data,sizeof(c_Data),0xFFFF);HAL_Delay(1000);
复制代码

  • 如果你想用 DMA 重定向(不卡 CPU)
    把上面的 HAL_UART_Transmit 换成 DMA 版本即可:
  1. int fputc(int ch, FILE *f){  // 等待上一次DMA发送完成  while(HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);    // DMA 发送单个字符  HAL_UART_Transmit_DMA(&huart1, (uint8_t *)&ch, 1);  return ch;}
复制代码

  • 必须开启的设置(重要!)
    如果发现 printf 不输出,99% 是这里没开:
    Keil 设置
    点击 魔法棒 → Target
    勾选 Use MicroLIB (使用微库)
    不勾这个,printf 不会工作!





5.PWM配置




5.1 STM32 PWM 模式 1 vs 模式 2(向上计数模式)
特性PWM mode 1PWM mode 2计数 < CCR 时有效电平 (High)无效电平 (Low)计数 ≥ CCR 时无效电平 (Low)有效电平 (High)占空比公式Duty=ARR+1CCR​×100%Duty=ARR+1ARR+1−CCR​×100%通俗理解高电平时间短 = 小占空比高电平时间长 = 大占空比低电平时间短 = 大占空比低电平时间长 = 小占空比

参数配置值含义与计算Prescaler (PSC)1440-1预分频器,实际值为 1439公式:PSC = 分频系数 - 1Counter ModeUp向上计数模式(最常用的 PWM 模式)Counter Period (ARR)100-1自动重装载值,实际为 99决定 PWM 周期:周期 = (PSC+1)*(ARR+1) / 定时器时钟Internal Clock Division (CKD)No Division时钟不分频,保持原定时器时钟Repetition Counter (RCR)0重复计数器,普通 PWM 无需使用auto-reload preloadDisable不开启自动重装载预装载(ARR 立即生效)

参数配置值详细说明ModePWM mode 1计数器值 < CCR 时输出有效电平,计数器值 ≥ CCR 时输出无效电平配合 CH Polarity=High,即:低占空比时输出低电平,高占空比时输出高电平Pulse (16 bits value)50捕获比较寄存器 CCR 的初始值,决定 PWM 占空比占空比 = CCR / (ARR+1)结合之前 ARR=99 的配置,占空比 = 50 / 100 = 50%Output compare preloadEnable开启 CCR 预装载功能,CCR 值在 ** 更新事件(UEV)** 时才生效,避免 PWM 输出出现毛刺、跳变,保证输出平滑Fast ModeDisable(高频输出PWM波)关闭快速模式,默认延迟一个时钟周期更新比较输出,适合常规 PWM 应用开启后可实现更快响应,但会增加硬件复杂度CH PolarityHigh通道有效电平为高电平若改为 Low,则有效电平为低电平,占空比逻辑反转CH Idle StateReset空闲状态时输出为复位电平(低电平)若改为 Set,则空闲时输出高电平5.2 输出比较模式 (Output Compare, OC)(不咋用到)

模式名称英文原名核心动作通俗解释适用场景Frozen(used for Timing base)不动作计数器与 CCR 匹配时,引脚电平保持不变定时器仅做计时,不控制输出引脚(最常用作基础时钟)Active Level on match匹配时置高输出 高电平计数到 CCR 时,引脚强制变 高产生一个单次启动信号(如启动电机)Inactive Level on match匹配时置低输出 低电平计数到 CCR 时,引脚强制变 低产生一个单次停止信号(如关断蜂鸣器)Toggle on match匹配时翻转电平翻转计数到 CCR 时,高变低,低变高产生方波(纯软件 PWM,无需 ARR 自动重载)Forced Active强制置高强制输出 高电平无论计数器是否到 CCR,引脚强制保持 高测试硬件通路,或强制拉高某个信号Forced Inactive强制置低强制输出 低电平无论计数器是否到 CCR,引脚强制保持 低测试硬件通路,或强制拉低某个信号5.3  PWM仿真
1.软件仿真(不需要单片机连接)















PWM输出的配置就已经完成了,但是不能输出产生PWM波,因为Cube在生成代码时,有很多外设初始
化完后默认是关闭的,需要我们手动开启。
  1. /* use code begin 2*/HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启定时器1 通道1 PWM输出HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_4);//开启定时器1 通道4 PWM输出/* use code end 2*/
复制代码
我们可以使用这个宏来修改占空比:__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 40);

2.硬件仿真(st-link or dap)



6.1-认识电机驱动
  市面上的驱动板比较多,我介绍一些常用的。1.L298N(与TB6612的使用思路差不多)

L298N 模块引脚STM32 引脚功能说明IN1Px电机 A 方向控制 1IN2Px电机 A 方向控制 2IN3Px电机 B 方向控制 1IN4Px电机 B 方向控制 2ENAPx (TIMx_CH1)电机 A 调速 PWM 输入ENBPx (TIMx_CH2)电机 B 调速 PWM 输入GNDGND必须共地,保证电平参考一致12V (可选)12V 直流电源正极电机驱动电源(7~12V 通用)GND12V 直流电源负极电机电源地,同时与 STM32 GND 共接5V(可选)5V 输入给 STM32 逻辑供电(若模块带 5V 稳压)OUT1电机 A 正极电机 A 输出端 1OUT2电机 A 负极电机 A 输出端 2OUT3电机 B 正极电机 B 输出端 1OUT4电机 B 负极电机 B 输出端 2控制逻辑对照表(方便编程)
电机IN1/IN3IN2/IN4ENA/ENB电机状态电机 A10PWM正转(调速)电机 A01PWM反转(调速)电机 A00任意停止电机 B10PWM正转(调速)电机 B01PWM反转(调速)电机 B00任意停止1.2例程使用代码
  1. //========== 方向引脚定义 ==========// L298N / TB6612 通用#define AIN1_SET    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET)#define AIN1_RESET  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET)#define BIN1_SET    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET)#define BIN1_RESET  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET)
复制代码
  1. void Motor_Set(int motor1, int motor2){    //  电机 B(motor1)    if(motor1 < 0)    {        BIN1_SET;   // 反转    }    else    {        BIN1_RESET; // 正转    }    if(motor1 < 0)    {        if(motor1 < -99) motor1 = -99;        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (100 + motor1));    }    else    {        if(motor1 > 99) motor1 = 99;        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, motor1);    }    //  电机 A(motor2)    if(motor2 < 0)    {        AIN1_SET;   // 反转    }    else    {        AIN1_RESET; // 正转    }    if(motor2 < 0)    {        if(motor2 < -99) motor2 = -99;        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, (100 + motor2));    }    else    {        if(motor2 > 99) motor2 = 99;        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, motor2);    }}
复制代码
2.TB6612



通过程序输入的AIN1,AIN2//BIN1,BIN2控制电机A组B组正反转,AO1,AO2,BO1,BO2接电机的端口,PWMA,PWMB接PWM信号输入端,其他的正常接,注意一定要按原理图的额定电压接(VM这一块)。

2.2 电机驱动信号表

2.3例程使用代码
  1. // TB6612 引脚定义#define AIN1_SET    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET)#define AIN1_RESET  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET)#define BIN1_SET    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET)#define BIN1_RESET  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET)```
复制代码
  1. void Motor_Set(int motor1, int motor2){    //  电机 B(motor1)    // 方向控制:正转/反转    if (motor1 < 0) BIN1_SET;   // 反转    else BIN1_RESET; // 正转    // 转速控制    if(motor1 < 0)    {        if(motor1 < -99) motor1 = -99;        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (100 + motor1));    }    else    {        if(motor1 > 99) motor1 = 99;        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, motor1);    }    //  电机 A(motor2)    // 方向控制    if(motor2 < 0) AIN1_SET;   // 反转    else AIN1_RESET; // 正转    // 转速控制    if(motor2 < 0)        {if(motor2 < -99) motor2 = -99;        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, (100 + motor2));}    else    { if(motor2 > 99) motor2 = 99; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, motor2);    }}
复制代码

3.AT8266/AT4985
1.原理图及功率表


2.接线图
DCDC12V,降压模块12V转V,编码器直流有刷电机,STM32。

3.工作原理


通过PB13—AIN1的高低电平进行电机的正反转(AIN2-AIN1=P(PWM值),其中P>0,电机正转,P 99) motor1 = 99;      //现在是 0 1 99      //我们赋值 0 1 99      __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, motor1);//修改定时器1 通道1PA8 Pulse改变占空比}//motor2 设置电机A的转速if(motor2 < 0){        if(motor2 < -99) motor2 = -99;//超过PWM幅值        //负的时候绝对值越小 PWM占空比越大        //现在的motor2 -1 -99        //给寄存器或者函数 99 1        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, (100+motor2));//修改定时器1 通道4 PA11 Pulse改变占空比        }else{        if(motor2 > 99) motor2 = 99;        //现在是 0 1 99        //我们赋值 0 1 99        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, motor2);//修改定时器1 通道4 PA11 Pulse改变占空比         }}[/code]这几款电机驱动的选择难舍难分,下面是几个电机的优缺点:
特性TB6612L298NA4950驱动类型双路 H 桥直流电机驱动双路 H 桥直流电机驱动双路 H 桥直流电机驱动电机驱动电压4.5-10V7-12V(最高 46V)7.6-30V逻辑供电电压2.7-5.5V(兼容 3.3V/5V 单片机)5V(需模块稳压)5V连续输出电流1.2A / 路(峰值 3.2A)1A / 路(峰值 2A)1.5A / 路(峰值 3A)导通电阻~0.14Ω(MOSFET,效率 > 90%)>2Ω(双极型,效率 60-70%)低 MOSFET(效率 > 85%)发热情况极低,无需散热片极高,必须加散热片低,小散热即可特殊功能STBY 待机控制(低功耗)无内置过流 / 过温 / 欠压保护典型场景电池供电小车、低功耗机器人高压大电流电机、低成本方案中高压电机、工业级场景TB6612:电池供电小车、低功耗、小体积,效率高、发热低、兼容 3.3V 单片机。
L298N:高压大电流电机、低成本、资料多电压范围宽、驱动能力强、入门友好。
A4950/AT8266:中高压工业场景、高可靠性,宽压输入、全保护。


7.1 认识编码器
一、编码器官方定义
编码器(Encoder)是一种将角位移、角速度等机械运动参数转换为脉冲信号或数字代码的传感器装置,通过检测旋转或直线运动的位置、速度与方向信息,实现运动系统的闭环控制与精准定位,广泛应用于电机调速、伺服控制、工业自动化、机器人、精密传动等领域。
编码器有很多种,有角速度编码器,正交编码器,方向编码器,绝对值编码器
增量式编码器:
每旋转一个固定角度输出一组脉冲,仅记录相对位移,断电后数据丢失,需复位归零。特点:结构简单、成本低、响应快、适合速度与相对位置检测。
绝对式编码器:每个位置对应唯一数字编码,可直接读取绝对位置,断电不丢失,开机即可知当前位置。特点:精度高、抗干扰强、成本高,用于高精度定位场景。
啥?
编码器,电机自带的 “小尺子 + 计数器”
电机 转了多少圈
转得 有多快(转速)
往 哪个方向转
没有编码器:你只能给 PWM,不知道电机实际转没转、转多快、会不会打滑。
有编码器:你就能做 精准调速、走固定距离、定角转动、平衡小车。
具体参数:
转速: 单位时间测量到的脉冲数量(比如根据每秒测量到多少个脉冲来计算转速)
旋转方向: 两通道信号的相对电平关系
编码器输出的波形,如何通过单片机读取波形,然后计算出速度?
STM32单片机的定时器和通用定时器具有编码器接口模式
如图:

这个是计数方向与编码器信号的关系、我们拆开来看
仅在TI1计数、电机正转、对原始数据二倍频

在TI1和TI2都计数
可以看到这样就对原始数据四倍频了

计数方向可以根据cubemx中的counter mode设置:

counter mode有三种形式:
Up(向上计数)
波形:0 → ARR → 0 → ARR 锯齿波(如下左图)
溢出:CNT 达到 ARR 后触发更新中断(UEV),归零重计
Down(向下计数)
波形:ARR → 0 → ARR → 0 反向锯齿波(如下右图)
下溢:CNT 达到 0 后触发更新中断(UEV),从 ARR 重计
Center-aligned(中心对齐)
波形:0 → ARR → 0 → ARR 三角波
特点:递增 / 递减阶段各触发一次中断,适合生成对称 PWM 波形。

我们要测量速度,因此要获得单位时间计数器值变化量,有两种方法:
这次编码器计数值 = 计数器值+计数溢出次数 * 计数最大器计数最大值
计数器两次变化值 = 这次编码器计数值 - 上次编码器计数值
然后根据这个单位变化量计算速度
计数器变化量 = 当前计数器值
每次计数值清空
然后根据这个变化量 计算速度
那么编码器是如何算出速度?
举个例子:
JGA25系列的两个不同减速比的编码器:
参数数值计算结果减速比1:30电机轴转 30 圈 → 输出轴转 1 圈编码器线数(电机轴)11 线 / 圈电机轴每转 11 个周期输出轴等效线数30×11 = 330 线 / 圈轮子每转 330 个周期4 倍频后总脉冲330×4 = 1320 脉冲 / 圈轮子每转计数器 + 1320空载转速(输出轴)~200 RPM低速大扭矩,适合 AGV 小车额定电压12V同系列通用参数数值计算结果减速比1:100电机轴转 100 圈 → 输出轴转 1 圈编码器线数(电机轴)11 线 / 圈电机轴每转 11 个周期输出轴等效线数100×11 = 1100 线 / 圈轮子每转 1100 个周期4 倍频后总脉冲1100×4 = 4400 脉冲 / 圈超高精度,定位细腻空载转速(输出轴)~60 RPM超低速,适合云台、机械臂关节额定电压12V轮子转速计算:  n=ΔN/(4×i×PPR)
为啥是4——默认encoder mod为Encoder Mode TI1 and TI2模式,4倍频
n:输出轴转速(RPM,转 / 秒)
ΔN:单位时间内定时器计数变化量(我程序写的是HAL_Delay(2),每两毫秒清除一次)
i:减速比
PPR:电机轴编码器线数(线 / 圈)
eg:
减速比 i=9.6,PPR=11,ΔN=422.4(1 圈计数值)
n=4×9.6×11422.4​×60=60转每分钟
那么在cubemx如何配置?如图:

开启定时器和定时中断
  1. HAL_TIM_Encoder_Start(&htim2,TIM_CHANNEL_ALL);//开启定时器2HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL);//开启定时器4HAL_TIM_Base_Start_IT(&htim2); //开启定时器2 中断HAL_TIM_Base_Start_IT(&htim4); //开启定时器4 中断
复制代码
在定义两个变量保存计数器值
  1. short Encoder1Count = 0;//编码器计数器值short Encoder2Count = 0;
复制代码
为啥是short?
STM32 的TIMx_CNT是16 位无符号寄存器,取值范围 0 ~ 65535;
为了彻底解决溢出问题,无需手动维护溢出次数,强制转换后,CNT 的溢出 / 下溢会自动变成连续的正负值,直接用差值计算位移,完全不用管溢出!
比如向上计数模式时CNT 从0递增到65535,溢出后归零。
0~32767 → 转 short 后为0~32767(正数,正转)
32768~65535 → 转 short 后为-32768~-1(负数,相当于正转溢出后的连续值,当然我们的电机也不会溢出,ΔN=422.4,32767/422.4=77.57339015*500(每两毫秒清除一次)=38786.695075转每秒)
燃油超跑(自然吸气 / 涡轮):
法拉利 12Cilindri:9500 转 / 分 → 158.3 转 / 秒
阿波罗 Evo:8500 转 / 分 → 141.7 转 / 秒
雪佛兰科尔维特 Z06:8600 转 / 分 → 143.3 转 / 秒
兰博基尼 Temerario:10000 转 / 分 → 166.7 转 / 秒
顶级赛道 / 复古高转:
Gordon Murray S1 LM:12100 转 / 分 → 201.7 转 / 秒
Rodin FZero(NA):12000 转 / 分 → 200.0 转 / 秒
F1 赛车(规则限制):15000 转 / 分 → 250.0 转 / 秒(比赛常用换挡约 12000–14000 转 / 分 → 200–233 转 / 秒)
6666你家电机比超跑快几百倍,所以我们的电机连续值也不会溢出,不必担心。
比如向下计数模式时
32768~65535 → 转 short 后为-32768~-1(负数,反转)
0~32767 → 转 short 后为0~32767(正数,相当于反转下溢后的连续值)

每2ms读取计数器值->清零计数器
我们用这种方法
计数器变化量 = 当前计数器值
每次计数值清空
然后根据这个变化量 计算速度
  1. Motor_Set(0,0);//1.保存计数器值Encoder1Count =(short)__HAL_TIM_GET_COUNTER(&htim4);Encoder2Count =(short)__HAL_TIM_GET_COUNTER(&htim2);//2.清零计数器值__HAL_TIM_SET_COUNTER(&htim4,0);__HAL_TIM_SET_COUNTER(&htim2,0);printf("Encoder1Count:%d\r\n",Encoder1Count);//oled显示printf("Encoder2Count:%d\r\n",Encoder2Count);//oled显示HAL_Delay(2);
复制代码
计算速度(假如单位设置为转每秒)
  1. //定义速度变量float Motor1Speed = 0.00;float Motor2Speed = 0.00;
复制代码
  1. //计算速度Motor1Speed = (float)Encode1Count*100/9.6/11/4;//计数值*100映射到0-100Motor2Speed = (float)Encode2Count*100/9.6/11/4;printf("Motor1Speed:%.2f\r\n",Motor1Speed);printf("Motor2Speed:%.2f\r\n",Motor2Speed);
复制代码
定时器(TIM)中断定时测量速度
上面我们实现:在主函数周期,读取计数器值然后计算速度,但是如果函数加入其他内容这个周期时间就很难保证。
为啥?
主循环是软循环,时间不可控,main 里有延时、有打印、有逻辑判断、有传感器读取,程序忙  周期变长,程序闲 周期变短
但是我们需要固定时间间隔如果时间不固定,同样脉冲数时间越长速度算得越慢,后面控制算法(PID)必须等周期运行,所以我们必须用定时器中断 + 固定周期采样,用定时器中断,可以保证无论主循环多忙,测速和控制周期永远精确不变。我们使用高级定时器 TIM1 产生 2ms 定时中断,在中断回调函数中完成编码器读取、速度计算、PID 运算和 PWM 输出。
因此:
我们先开启定时器、2ms进入一次定时器中断,中断回调函数执行代码即可,设置内部时钟源,使能自动重装载,开启定义更新中断。

  1. HAL_TIM_Base_Start_IT(&htim1); //开启定时器1 中断
复制代码
定时器回调函数中添加 速度计算内容
  1. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){    if(htim == &htim1)  // TIM1 2ms 中断    {        static uint8_t TimerCount = 0;        TimerCount++;        if(TimerCount >= 5)  // 10ms 执行一次        {            // 读取编码器            Encode1Count = (int16_t)__HAL_TIM_GET_COUNTER(&htim4);            Encode2Count = (int16_t)__HAL_TIM_GET_COUNTER(&htim2);            // 清零            __HAL_TIM_SET_COUNTER(&htim4, 0);            __HAL_TIM_SET_COUNTER(&htim2, 0);            // 计算转速 RPM            Motor1Speed = (float)Encode1Count * 10.0f / 11.0f /4.0f;            Motor2Speed = (float)Encode2Count * 10.0f / 11.0f /4.0f;            TimerCount = 0;        }    }}
复制代码
if(htim == &htim1)
判断进入的中断是否为 TIM1 高级定时器
TimerCount++;
每 2ms 进入一次中断,计数器自增
if(TimerCount %5 == 0)
2ms × 5 = 10ms
表示每 10ms 读取一次编码器、计算一次速度
读取编码器,清空编码器计数器,速度计算,
转速(RPM) = 10ms脉冲数 × 100(1秒=10个10ms)
÷ 9.6(减速比?或倍频系数)
÷ 11(编码器线数 PPR)
÷ 4(四倍频)
  1. //定义变量short Encode1Count = 0;short Encode2Count = 0;float Motor1Speed = 0.00;float Motor2Speed = 0.00;uint16_t TimerCount=0;
复制代码
主函数就输出速度大小
  1. printf("Motor1Speed:%.2f\r\n",Motor1Speed); printf("Motor2Speed:%.2f\r\n",Motor2Speed);
复制代码
外部变量需要声明一下
  1. extern float Motor1Speed ;extern float Motor2Speed ;
复制代码

注意:根据电机和实际调整速度测量与占空比设置函数
由于硬件接线的天然反向
在小车电机接线中,左右电机安装方向相反或驱动板(TB6612/L298N/AT8266/AT4985)的方向引脚电平定义或电机正负极接线顺序等原因需要改一下,每个人的接线方式不同正负号加的地方也不一样。
找到motor_set 函数
  1. void Motor_Set (int -motor1,int -motor2)//改一下{//根据参数正负 设置选择方向if(motor1 < 0) BIN1_SET;else BIN1_RESET;if(motor2 < 0) AIN1_SET;else AIN1_RESET;.....
复制代码
找到HAL_TIM_PeriodCallback函数
  1. void HAL_TIM_PeriodCallback(TIM_HandleTypeDef *htim){    if(htim == &htim1)  // TIM1 2ms 中断    {        static uint8_t TimerCount = 0;        TimerCount++;        if(TimerCount >= 5)  // 10ms 执行一次        {            // 读取编码器            Encode1Count = -(int16_t)__HAL_TIM_GET_COUNTER(&htim4);//加负号            Encode2Count = (int16_t)__HAL_TIM_GET_COUNTER(&htim2);.....
复制代码
9.PID-速度控制
你的小车很多时候的运动参数不是恒定不变的,之前的 Motor_Set(50) 只是开环给 PWM,如果出现电池电压下降,速度变慢,小车载重 ,速度变慢,左右电机转速不一致 ,小车跑偏等情况,咋办?
我们先编写一个简单的控制方法
要求:转速控制在3.9-4.0转每秒
  1. if(Motor1Speed>4.0) Motor1Pwm--;if(Motor1Speed4.0) Motor2Pwm--;if(Motor2Speedactual_val = actual_val;//传递真实值    pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值   // 纯P输出 = Kp * 误差    pid->actual_val = pid->Kp*pid->err;    return pid->actual_val;//返回真实值}//使用PI控制 输出=Kp*当前误差+Ki*误差累计值//比例P 积分I 控制函数float PI_realize(tPid * pid,float actual_val){     pid->actual_val = actual_val;//传递真实值     pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值     pid->err_sum += pid->err;//误差累计值 = 当前误差累计和     //使用PI控制 输出=Kp*当前误差+Ki*误差累计值     pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum;     return pid->actual_val;}// PID控制函数float PID_realize(tPid * pid,float actual_val){   pid->actual_val = actual_val;//传递真实值   pid->err = pid->target_val - pid->actual_val;////当前误差=目标值-真实值   pid->err_sum += pid->err;//误差累计值 = 当前误差累计和//使用PID控制 输出 = Kp*当前误差 + Ki*误差累计值 + Kd*(当前误差-上次误差)   pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid->err - pid->err_last);//保存上次误差: 这次误差赋值给上次误差   pid->err_last = pid->err;   return pid->actual_val;}
复制代码
3.定义一个 PID 结构体变量
创建一个 电机 1 速度控制的 PID 对象
所有误差、目标、实际值、参数都存在这里面
tPid pidMotor1Speed;
然后在pid.h中声明函数与定义:
  1. #ifndef __PID_H#define __PID_H//声明一个结构体类型typedef struct{    float target_val;//目标值    float actual_val;//实际值    float err;//当前偏差    float err_last;//上次偏差    float err_sum;//误差累计值    float Kp,Ki,Kd;//比例,积分,微分系数} tPid;//声明函数    float P_realize(tPid * pid,float actual_val);    void PID_init(void);    float PI_realize(tPid * pid,float actual_val);    float PID_realize(tPid * pid,float actual_val);#endif
复制代码
然后在main中要调用PID_init()即可
理论有了,那么如何用理论解决实际呢(也就是如何控制电机速度?)
我们这里增加一个pid电机控制函数,将PWM与pid集合在一起。
  1. void motorPidSetSpeed(float Motor1SetSpeed,float Motor2SetSpeed){//改变电机PID参数的目标速度pidMotor1Speed.target_val = Motor1SetSpeed;pidMotor2Speed.target_val = Motor2SetSpeed;//根据PID计算 输出作用于电机Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),PID_realize(&pidMotor2Speed,Motor2Speed));}Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),PID_realize(&pidMotor2Speed,Motor2Speed));
复制代码
比如控制一个小车:motorPidSetSpeed(1,2):1电机是左电机转速,2电机是右电机转速。(单位为每转每秒)
  1. // motorPidSetSpeed(1,2);//向右转弯// motorPidSetSpeed(2,1);//向左转弯// motorPidSetSpeed(1,1);//前进// motorPidSetSpeed(-1,-1);//后退// motorPidSetSpeed(0,0);//停止// motorPidSetSpeed(-1,1);//右原地旋转// motorPidSetSpeed(1,-1);//左原地旋转
复制代码
还可以实现向前加减速
  1. //向前加速函数void motorSpeedUp(void){static float MotorSetSpeedUp=0.5;//静态变量 函数结束 变量不会销毁if(MotorSetSpeedUp =0.5) MotorSetSpeeddown-=0.5;//判断是否速度太小motorPidSetSpeed(MotorSetSpeeddown,MotorSetSpeeddown);//设置到电机}
复制代码
让OLED显示速度与总路程
这里我们只要计算出每个单位时间内小车行驶的长度然后一直相加,就是这一段时间行驶的总里程长度。
比如让我们20ms计算一次,20ms走过了多少距离,然后一直相加,就是走的总距离。这里我们可以使用
电机1进行计算;也可以电机1 和电机2相加然后除2。
  1. if(TimerCount%10==0)//基础中断周期:2ms(TIM 中断每 2ms 触发一次,每 10 次中断(2ms×10=20ms)执行一次后续代码{    //里程数(cm) += 时间周期(s)*车轮转速(转/s)*车轮周长(cm)    Mileage += 0.02*Motor1Speed*22;    Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),PID_realize(&pidMotor2Speed,Motor2Speed));    TimerCount=0;}
复制代码
主函数我们通过OLED显示电机速度和小车里程
  1. sprintf((char *)OledString,"V1:%.2f V2:%.2f", Motor1Speed,Motor2Speed);//显示两个电机的速度OLED_ShowString(0,0,OledString,12);//这个是oled驱动里面的,(X 坐标,Y 坐标,要显示的内容(字符串 / 数组),字体大小)sprintf((char *)OledString,"Mileage:%.2f ",Mileage);//显示里程数OLED_ShowString(0,1,OledString,12);
复制代码
10循迹功能,红外对管,灰度传感器
红外对管传感器的工作机制主要建立在红外光的发射、反射或直射接收以及信号处理的基础上。传感器的发射器和接收器同样集成在一体。发射管发出的红外光遇到被测物体后,依靠物体自身的表面将光漫反射回接收管。接收管接收到足够强度的反射光后触发信号输出。这种方式的检测距离较短,且受物体颜色、表面反光率影响较大(白色物体易反射,黑色物体易吸收);常用TCRT5000红外。
TCRT5000红外:
根据传感器特性,我们检测红外对管DO引脚的电压就可以知道,下面有黑线,DO高电平,开关灯灭,黑色是不反射红外线,当循迹模块遇到黑线,模块输出高电平D0=1,输出指示灯熄灭LED=1。
没有黑线,DO低电平,开关灯亮,红外发射器一直发射红外线,红外线经发射后被接收,此时输出低电平D0=0,输出指示灯点亮LED=0。

咋接?
引脚功能接线说明VCC电源正极接 3.3V/5V 电源(推荐 5V 提升发射功率)GND电源负极接单片机 GND,共地DO数字信号输出接 STM32 GPIO 引脚,用于开关量检测AO模拟信号输出接 STM32 ADC 引脚,用于灰度 / 距离检测(可选,一般循迹可不接)下面我们通过单片机读取红外对管DO口的电压,就知道黑线在车下面的位置了。
1.STM32初始化
比如:OUT_1-PA1、OUT_2-PA2、OUT_3-PB0、OUT_4-PB1初始化为输入模式,假如
2.添加读取GPIO的宏
  1. #define READ_HW_OUT_1 HAL_GPIO_ReadPin(HW_OUT_1_GPIO_Port,HW_OUT_1_Pin) //读取红外对管连接的GPIO电平#define READ_HW_OUT_2 HAL_GPIO_ReadPin(HW_OUT_2_GPIO_Port,HW_OUT_2_Pin)#define READ_HW_OUT_3 HAL_GPIO_ReadPin(HW_OUT_3_GPIO_Port,HW_OUT_3_Pin)#define READ_HW_OUT_4 HAL_GPIO_ReadPin(HW_OUT_4_GPIO_Port,HW_OUT_4_Pin)
复制代码
3.根据红外对管状态控制电机速度
  1. if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4== 0 ){    printf("应该前进\r\n");    motorPidSetSpeed(1,1);//前运动}if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 1&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4== 0 ){    printf("应该右转\r\n");    motorPidSetSpeed(0.5,2);//右边运动}if(READ_HW_OUT_1 == 1&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4== 0 ){    printf("快速右转\r\n");    motorPidSetSpeed(0.5,2.5);//快速右转}if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 1&&READ_HW_OUT_4== 0 ){    printf("应该左转\r\n");    motorPidSetSpeed(2,0.5);//左边运动}if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4== 1 ){    printf("快速左转\r\n");    motorPidSetSpeed(2.5,0.5);//快速左转}
复制代码
红外由于是光敏传感器易受到的环境光干扰,那么怎么办才能消除环境光干扰?
那么可以使用软件补偿的方式 :先开灯测总光,再关灯测背景光,做差值得到净反射值。
  1. uint16_t read_reflectance(void) {    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 500);  // 开启LED,用 PWM 开启,红外灯亮,发射红外光,让地面反射。    HAL_Delay(1);//等待 1ms,让红外光稳定、接收管响应稳定    uint16_t bright = ADC_ReadChannel(READ_HW_OUT_1);     __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 0);   // 关闭LED    HAL_Delay(1);wwdd    uint16_t dark = ADC_ReadChannel(READ_HW_OUT_1);     return bright - dark;  // 净反射值,大幅削弱恒定背景光影响}
复制代码
加入PID循迹
前面的代码我们对循迹功能的实现是代码直接判断几个状态,然后PID控制电机不同速度,现在可以使用红外对管状态作为PID控制的输入然后再控制电机。我们设计PID输入是红外对管的状态、然后输出一个速度值,然后左右电机去加或者减这个速度值,就可以完成根据红外对管输入对电机的差速控制。(本人“if else”终生受益者(dog),如有更好的寻线思路,请私信我)
  1. uint8_t g_ucaHW_Read[4] = {0};//保存红外对管电平的数组g_ucaHW_Read[0] = READ_HW_OUT_1;//读取红外对管状态g_ucaHW_Read[1] = READ_HW_OUT_2;g_ucaHW_Read[2] = READ_HW_OUT_3;
复制代码
[code]g_ucaHW_Read[0] = READ_HW_OUT_1;//读取红外对管状态g_ucaHW_Read[1] = READ_HW_OUT_2;g_ucaHW_Read[2] = READ_HW_OUT_3;g_ucaHW_Read[3] = READ_HW_OUT_4;if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==0&&g_ucaHW_Read[3] == 0 ){    g_cThisState = 0;//前进}else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] ==0&&g_ucaHW_Read[3]== 0 ){    g_cThisState = -1;//应该右转}else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==0&&g_ucaHW_Read[3] == 0 ){    g_cThisState = -2;//快速右转}else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] ==0&&g_ucaHW_Read[3] == 0){    g_cThisState = -3;//快速右转}else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==1&&g_ucaHW_Read[3] == 0 ){    g_cThisState = 1;//应该左转}else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==0&&g_ucaHW_Read[3] == 1 ){    g_cThisState = 2;//快速左转}else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] ==1&&g_ucaHW_Read[3] == 1){    g_cThisState = 3;//快速左转}//动态降速// 偏离越大 → 速度越慢    int abs_state = (g_cThisState < 0) ? -g_cThisState : g_cThisState;    if(abs_state == 0)      g_fBaseSpeed = 5.0;  // 中间 → 全速    else if(abs_state == 1) g_fBaseSpeed = 4.0;  // 微偏 → 稍减速    else if(abs_state == 2) g_fBaseSpeed = 3.0;  // 中偏 → 减速    else                    g_fBaseSpeed = 2.0;  // 大偏 → 很慢g_fHW_PID_Out = PID_realize(&pidHW_Tracking,g_cThisState);//PID计算输出目标速度这个速度,会和基础速度加减g_fHW_PID_Out1 = 3 + g_fHW_PID_Out;//电机1速度=基础速度+循迹PID输出速度g_fHW_PID_Out2 = 3 - g_fHW_PID_Out;//电机1速度=基础速度-循迹PID输出速度if(g_fHW_PID_Out1 >5) g_fHW_PID_Out1 =5;//进行限幅 限幅速度在0-5之间if(g_fHW_PID_Out1 5) g_fHW_PID_Out2 =5;if(g_fHW_PID_Out2

相关推荐

您需要登录后才可以回帖 登录 | 立即注册