本文将记录我的学习历程,基于rm机甲大师实验室,主要包括各种函数的应用及相应硬件模块的介绍。阅读需要有51单片机和基本的寄存器基础。
stm32cubemx时钟树配置
12 HSE /6 x168 /2 PLLCLK /4 /2
c语言基础
一 定义结构体 struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } book;//此声明声明了拥有四个变量的结构体,同时声明了结构体变量book,它的类型(标签)是struct Books struct Books book2,book3[20],*book4;//用struct Books标签又声明了三个结构体变量 //也可以用typedef创建新类型 typedef struct { char title[50]; char author[50]; char subject[100]; int book_id; } Books2; //现在可以用Books2作为类型声明新的结构体变量 Books2 book5,book6[20],*book7; 结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针(在代码中常见) //此结构体的声明包含了其他的结构体 struct COMPLEX { char string[100]; struct Books book; }; //此结构体的声明包含了指向自己类型的指针 struct NODE { char string[100]; struct NODE *next_node; }; 二 结构体变量的初始化 #include
struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } book = {"C 语言", "RUNOOB", "编程语言", 123456}; int main() { printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id); } 三 访问结构成员 非指针用“.”,指针用“->” 如struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } book book.title book.author ...... 如struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } *book2 book2->title book2->author ....... book2=&book
1 原理:三个 LED 灯的引脚为 PH10,PH11,PH12。在user label 填写命名LED_B LED_G LED_R 对应引脚输出高电平点亮。
2 代码:
点亮LED
HAL_GPIO_WritePin(LED_R_GPIO_Port, LED_R_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_G_GPIO_Port, LED_G_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_B_GPIO_Port, LED_B_Pin, GPIO_PIN_SET);
或
HAL_GPIO_WritePin(GPIOH, GPIO_PIN_12, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOH, GPIO_PIN_11, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOH, GPIO_PIN_10, GPIO_PIN_SET);
LED闪烁
HAL_GPIO_TogglePin(GPIOH,GPIO_PIN_10);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOH,GPIO_PIN_11);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOH,GPIO_PIN_12);
HAL_Delay(500);
参数:端口、引脚 功能:对应端口的引脚电平翻转
蓝色-青色(蓝色和绿色混合)-白色(红蓝绿混合)-重灰色(红绿混合)-红色-灭
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
//读取对应引脚的电平并返回
1 原理:
定时器:分配+计数+重载
(1)预分频寄存器TIMx_PSC:时钟源的信号经过TIMx_PSC按其中的分频比Prescaler进行分频(频率除以Prescaler)
(2)自动重装载寄存器 TIMx_ARR :计数器寄存器TIMx_CNT 根据时钟的频率向上计数,直到 等于TIMx_ARR的自动重装载值Counter Period,产生一个定时中断触发信号,TIMx_CNT 被清空, 并重新从 0 开始向上计数。
定时器周期=[(分频值+1)(重载值+1)]/时钟源频率
中断:当多个中断发生时,先根据抢占优先级判断哪个中断分组能够优先响应,再到这个中断分组 中根据各个中断的响应优先级判断哪个中断优先响应。
it.c文件中有我们配置好使能的中断类型,找到这个中断服务函数。 里面有中断回调函数,我们在主文件中对此进行重新编写。 1 HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim) 参数:*htim 定时器的句柄指针,如定时器 1 就输 入&htim1,定时器 2 就输入&htim2 作用:中断服务函数 2 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 参数:&htim1... 作用:中断回调函数。 我们可以在别处重写中断回调函数,一般我们需要在中断回调函数中判断中断来源并执行相应的用户操作。 (如定时器1啊等等 if(htim == &htim1)) 3 HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim) 参数:略 HAL_StatusTypeDef是HAL 库定义的几种状态, 如果成功使定时器开始工作,则返回 HAL_OK 作用:使定时器开始工作 4 HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim) 参数:略 作用:使对应的定时器开始工作,并使能其定时中断 定时器配置-使能定时器中断-定时器计数值满进入中断-中断内部先进入中断服务函数再进入中断回调函数
2 代码
main.c文件中:
主函数:
各种初始化(包含定时器的初始化)//此部分内容配置好了,不用自己写
HAL_TIM_Base_Start_IT(&htim1);//开启定时器1的中断模式,开始工作
中断回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim1)
{
//500ms trigger
bsp_led_toggle();
}
}/*主函数开启定时器1以后,每到达时间就会进入中断服务函数中的中断回调函数。
中断回调函数在hal库的别的文件下有虚定义__weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim),我们需要在主文件下重写它。
进入主文件下的中断回调函数以后,首先判断是不是定时器1,再实现LED灯的翻转。*/
1 原理:
脉冲调制有两个重要的参数:
1 输出频率,频率越高,数字信号代替模拟信号的效果越好。
2 占空比。占空比就是改变输出模拟效果的电压大小。占空比越大则模拟出的电压越大。
算法原理:
1 颜色:16进制对应4位。32位的aRGB变量每8位分别代表了 alpha(透明度)Red(红色)Green(绿色)和 Blue(蓝色)四个要素。
如纯红色表示为 0xFFFF0000(透明度位置FF,红色位置FF,其他位置0),纯绿色表示为 0xFF00FF00,纯蓝色表示为 0xFF0000FF。黄色由蓝色和绿色合成,所以可以表示为 0xFF00FFFF。
通过移位操作获得每个颜色表示的八位变量,如red = ((RGB_flow_color[i] & 0x00FF0000) >> 16)。然后将这个值作为定时器通道的比较值控制LED灯的亮度调节。
//以上这段非人话,个人认为例程的这段代码写复杂了,没必要。
2 定时器:定时器设置为PWM模式。配置定时器中的比较寄存器TIMx_CCRx,计数寄存器的值不断增加,小于比较值的时候PWM输出高电平,大于比较值的时候PWM输出低电平。
占空比 =( _ − 1 )/_ ∗ 100%。
3 定时器的pwm模式:
每一个定时器的pwm模式有4个通道,每一个通道都有对应标号的比较寄存器,比如定时器5的 1 号通道对应的比较寄存器为 TIM5_CCR1。
可以注意到5号定时器三个通道对应的引脚正是之前的实验中使用的 LED 引脚,所以如果对5号定时器的通道的比较值进行配置,就能够控制每一个LED引脚的电压大小,从而改变灯的亮度。
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue); 参数:哪个定时器,哪个通道,比较的值。blue为变量,可变。 作用:通过这个函数将比较值赋值给对应定时器通道的比较寄存器 比较寄存器的值一直在变,输出电压一直在变,灯亮度改变。
2 代码:
main.c文件中:
主函数:
//开启定时器
HAL_TIM_Base_Start(&htim5);
//开启PWM通道,分别对应三个LED灯的引脚。LED和定时器硬件都连在这一个引脚上,定时器控制的电压变化那么灯亮度就变化。
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_3);
其他略
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, green);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_3, red);
1 原理:
蜂鸣器: 输入方波频率的不同产生不同的音调。蜂鸣器使用的引脚为 PD14,为定时器 4 的通道 3。Pulse可以设置比较寄存器的初始值。
时钟的频率是72MHZ,可以理解为一秒钟STM32会自己数72M次;分频系数是72,则该时钟的频率会变成72MHZ/72=1MHZ,一秒钟STM32会自己数1M次,但是在设置的时候要注意,数值应该是72-1;需要定时1ms,由于1ms=1us*1000,那么预装载值就是1000-1。在波形图上分频系数决定了多久来一个点(太快了所以连点成线),重载值决定了波形周期,比较值决定了高电平的时间。
__HAL_TIM_PRESCALER(&htim4, psc)//设置定时器4的分屏系数,HAL库中重载值也可以设置 __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, pwm);//设置定时器4的通道3的比较值 //psc和pwm都是一个变化的值,所以蜂鸣器连接引脚的电压一直在变化,发出音调变化
舵机:舵机的引脚为定时器1的通道1、2、3、4和定时器8的通道1、2、3。
时钟源/(分频+1)/(重载值+1)=50
舵机使用的 PWM 信号一般为频率 50Hz,舵机的控制一般需要一个20ms的时基脉冲,该脉冲的高电平部分一般为0.5ms~2.5ms范围内的角度控制脉冲部分,以180度电机为例:0.5ms————–0度;1.0ms————45度;1.5ms————90度;2.0ms———–135度;2.5ms———–180度。
MG995舵机,根据输入pwm占空比的不同调节转动的角度。如果我们想要用遥控器操纵舵机,那么首先遥控器初始化(包含DMA初始化),对遥控器遥感变动时候相应变动的寄存器进行编写(当遥控器寄存器大于小于xx值的时候,pwm咋样咋样),实现控制舵机的效果。具体见例程PWM_SNALL。
2 代码:设置占空比就行,略。
1 原理:
外部中断通常是 GPIO 的电平跳变引起的中断。在 stm32 中,每一个 GPIO 都可以作为外 部中断的触发源,外部中断一共有 16 条线,对应着 GPIO 的 0-15 引脚,每一条外部中断 都可以与任意一组的对应引脚相连。上升沿中断、下降沿中断、上升沿和下降沿中断。
配置:将相应引脚设置为外部中断模式,然后记得使能外部中断。
外部中断回调函数HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin),在主文件中重写,首先判断是哪个引脚产生的外部中断看看对不对。每当产生一次外部中断就进入中断服务函数先对中断寄存器进行处理然后进入中断回调函数。
注意消抖。
2 代码:
过几天自己写一个
1 原理:
(1)概念:分辨率=量程/2的n次方,代表一格数字量所反映的电压等模拟输入量。
(2)过程:先采样,再保持,再量化(变成能编码的量。如输入电压量程为4v,位数为3位,那么分辨率是0.5,若采样到的信号大小是3.6v无法编码,所以需要量化为3.5v,这样才能表示为111),最后编码成数字量给单片机。
(3)怎么测我们需要的地方的电压:ADC3的通道8连接电源,可以测电源的输出电压;其他通道连接其他地方,可以测其他内部元件的输出电压。通过ADC,可以将电机、各种传感器等的模拟量转换为单片机处理的数字量。
(4)程序思路:从程序角度阻塞式先开启ADC,然后判断是否转化完成,最后读取通道中ADC的值;非阻塞式的话开启ADC,然后去中断函数里面读取ADC的值。
非中断: HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc) 开启ADC的采样,如果是adc1就输入&hadc1,adc2就输入&adc2,adc3就输入&hadc3 HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout) 等待 ADC转换结束 uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc) 获取ADC值 中断: HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc) 开启ADC的采样 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) ADC的中断回调函数,我们在此之中写代码
2 代码:
过几天自己写一个
1 原理:
串口通讯,收发双方要遵从同样的协议才能完成数据传输,同时波特率也要相等,波特率可以设置为115200,38400,9600 等。
//使能接收中断和空闲中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); //receive interrupt __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //idle interrupt __HAL_UART_ENABLE_IT(&huart6, UART_IT_RXNE); //receive interrupt __HAL_UART_ENABLE_IT(&huart6, UART_IT_IDLE); //idle interrupt 发送函数 HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); //如串口1就输入&huart1;发送的数据的首地址,比如要发送buf[]=”Helloword”则输入buf;要发送的数据大小,可以用sizeof;等待的时间 接收函数 HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); 发送中断函数 HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); 接收中断函数 HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
串口接收中断即每当串口完成一次接收之后触发一次中断;串口空闲中断即每当串口接收完一帧数据后又过了一个字节的时间没有接收到任何数据则触发一次中断。
当串口发生接收中断或者空闲中断时,都会进入 USARTx_IRQHandler 中断处理函数。在中 断处理函数中通过串口的状态寄存器来判断产生中断的是接收中断还是空闲中断,然后进入相应的回调函数处理。
即在HAL中有中断服务函数void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
这个库函数帮我们完成了中断类型判断和清除标志位,我们只需要在具体的函数中写逻辑即可。上面这个库函数判断出不同的类型,然后调用不同的回调函数,我们处理接收中断回调函数HAL_UART_TxCpltCallback即可。
回调函数如下: 1 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); 2 void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); 3 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); 4 void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart); 5 void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart); 6 void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart); 7 void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart); 8 void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart);
2 代码:
1 原理:
DMA:DMA 是在使用串口进行通讯时常用的一个功能,使用该功能能够完成串口和内存之间直接的数据传送,而不需要 CPU 进行控制,从而节约 CPU 的处理时间。
缓冲区的作用是:数据通过DMA串口传进来以后我们要先在缓冲区中保存,然后进行一系列操作处理判断检验接收到的数据是否为我们需要的那个数据(判断对不对,通过比较位数是否相等的方式),若正确再放在内存中。缓冲区是DMA模式下内存中的一片区域,我们理解为数据进入内存存储前的一片区域即可。
注意:串口DMA的初始化代码是要自己写的,可以从例程中复制。
代码思路:RC_init(内含USART3的DMA的初始化:使能DMA串口接收-使能空闲中断(空闲中断要信号触发后过一个字节的时间才能空闲中断标志位置1,所以时间足够底下我们的初始化程序全部完成以后才会进中断服务函数对应的空闲中断触发部分)-失效DMA-配置缓冲区(缓冲区的位置在哪,能容纳的数据长度多少)-使能缓冲区-使能DMA)
遥控器寄存器的值是从-660到660(在代码里已经减去相应的量了)。
右遥控器前后是通道1,前正后负;左右是通道0,左负右正
左遥控器左右是通道3,前正后负;左遥控器左右是通道2,左负右正左边开关上中下对应s1 1,3,2
右边开关上中下对应s0 1,3,2
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Senbuff, sizeof(Senbuff)); //串口发送Senbuff数组 HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); //串口通过DMA接受指定长度的数据
串口1的DMA用于发送数据,即单片机向电脑发送数据(显示一些信息);串口3的DMA用于接收数据,即遥控器外设发送数据单片机的存储区域接收。
2 代码:
const RC_ctrl_t *local_rc_ctrl;//local_rc_ctrl是RC_ctrl_t变量类型的指针结构体变量名
remote_control_init();//红外遥控的初始化,内含接收串口3的DMA初始化
usart1_tx_dma_init();//发送串口1的DMA初始化
//以上这些初始化代码都要自己写,红外遥控初始化拷贝相应文件,发送串口1初始化见后。
local_rc_ctrl = get_remote_control_point();//获取红外遥控器的指针,它就可以指向红外遥控器的各种寄存器。
//local_rc_ctrl->rc.ch[0], local_rc_ctrl->rc.ch[1], local_rc_ctrl->rc.ch[2]等
//发送串口1初始化
void usart1_tx_dma_init(void)
{
//enable the DMA transfer for the receiver request
//使能DMA串口接收
SET_BIT(huart1.Instance->CR3, USART_CR3_DMAT);
}
void usart1_tx_dma_enable(uint8_t *data, uint16_t len)
{
//disable DMA
//失效DMA
__HAL_DMA_DISABLE(&hdma_usart1_tx);
while(hdma_usart1_tx.Instance->CR & DMA_SxCR_EN)
{
__HAL_DMA_DISABLE(&hdma_usart1_tx);
}
//clear flag
//清除标志位
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_HISR_TCIF7);
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_HISR_HTIF7);
//set data address
//设置数据地址
hdma_usart1_tx.Instance->M0AR = (uint32_t)(data);
//set data length
//设置数据长度
hdma_usart1_tx.Instance->NDTR = len;
//enable DMA
//使能DMA
__HAL_DMA_ENABLE(&hdma_usart1_tx);
}
关于通信协议:
主从通信:主机轮(流)询(问),从机应答。发送指令给从机,让它执行什么操作;向从机要数据。
每一次通信都必须由主机发起,从机不可以主动向主机发送数据。系统上电以后所有的设备都处于接收状态,所以应当先将主机调成发送模式,发送数据包; 主机转成接收模式,接收从机发送的应答;
地址码就是从机的地址,我们需要和哪一台设备建立联系(即ID)
功能码就是我们需要对从机执行什么操作(读/写,单个位/多个寄存器等等)
数据码就是我们往寄存器/位里面塞这样一个数,它本身就代表了某种信息(控制指令)
个人计算机都均有内存和硬盘(外存),开发板芯片 stm32 同样具有内存 192Kbytes 的 SRAM 和 1Mbytes 的外存 flash。flash是闪存外设,可以把数据写进去存储,也可以从里面读取数据。
flash的优点是容量较大,掉电不会丢失数据;缺点是写入需要先擦除,读取写入速度较慢。
注意:1. 为了保护数据的安全性,flash 有专门的锁寄存器,每次要对 flash 页面进行修改时首先 要通过锁寄存器对页面进行解锁,修改完成后要进行加锁。2. flash 是不支持在保存原有数据的情况下进行修改的,因此要改变 flash 页面数据时,需 要对这个页面进行擦除,擦除之后再写入新的数据。
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError
//flash擦除函数,擦除指定的flash页面。
//参数:擦除 flash 时使用的结构体指针,需要创建一个 FLASH_EraseInitTypeDef 类型的结构体 flash_erase,
需要赋予这个结构体以下参数:Sector(要擦除的页面的首地址),TypeErase(擦除方式),VoltageRange(电压范围),NbSectors(待擦除页面数),最后我们将&flash_erase 作为参数输入函数;
如果本次 flash 擦除产生了错误,则发生擦除错误的页面号存储在 SectorError 中
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data)
//flash写入函数,以指定的方式,向 flash 中的一页写入数据
参数:uint32_t TypeProgram 选择写入的数据格式,可以选择8位字节FLASH_TYPEPROGRAM_BYTE , 16 位半字FLASH_TYPEPROGRAM_HALFWORD ,
32 位 字FLASH_TYPEPROGRAM_WORD ,或者 64 位双字FLASH_TYPEPROGRAM_DOUBLEWORD
需要写入数据的地址;需要写入的数据
void flash_read(uint32_t address, uint32_t *buf, uint32_t len)
{
memcpy(buf, (void*)address, len *4);
}
//从 flash 读取数据
//参数:Flash 地址;读取后的存储变量地址;字节长度
#define USER_FLASH_ADDRESS ADDR_FLASH_SECTOR_11
#define FLASH_DATA_LENGHT 12
//ADDR_FLASH_SECTOR_11即0x080E0000,是12个flash叶分区的第一个
uint8_t after_erase_data[FLASH_DATA_LENGHT];
uint8_t write_data[FLASH_DATA_LENGHT] = "RoboMaster\r\n";
uint8_t after_write_data[FLASH_DATA_LENGHT];
//擦除flash页
flash_erase_address(USER_FLASH_ADDRESS, 1);
//read data from flash, before writing data
//在写数据之前,从flash读取数据
flash_read(USER_FLASH_ADDRESS, (uint32_t *)after_erase_data, (FLASH_DATA_LENGHT + 3) / 4);
//write data to flash
//往flash写数据
flash_write_single_address(USER_FLASH_ADDRESS, (uint32_t *)write_data, (FLASH_DATA_LENGHT + 3) / 4);
//read data from flash, after writing data
//在写数据之后,从flash读取数据
flash_read(USER_FLASH_ADDRESS, (uint32_t *)after_write_data, (FLASH_DATA_LENGHT + 3) / 4);
串行同步通讯总线协议--I2C,使用 I2C可以配置和读取 IST8310 磁力计的数据,还可以用于温度传感器,气压传感器,多路 ADC 模块等多种传感器。
IST8310 磁力计:测量地球磁场强度,用于计算机器人的朝向。
SCL、SDA分别为I2C的时钟线和数据线,RSTN为IST8310 的 RESET,低电平重启 IST8310,DRDY为IST8310 的数据准备(data ready)。
程序开始先进行 HAL 库自带的初始化,包括时钟,GPIO,I2C3 的初始化;之后完成配置 IST8310,IST8310 的 DRDY 引脚会产生 200Hz 的周期信号;当 DRDY 下降沿,会引起单 片机的下降沿外部中断;在外部中断回调函数中,调用 ist8310 的读取函数,便可以读取磁 场数据。
I2C通信协议
I2C 有两根信号线,一根数据线 SDA,另一根是时钟线 SCL。I2C 总线允许挂载多个主设备,但总线时钟同一时刻只能由一个主设备产生,并且要求每个 连接到总线上的器件都有唯一的 I2C 地址,从设备可以被主设备寻址。
整个过程便是如同到学校的快递柜(从机 I2C 地址),对第几号柜箱(寄存器地址), 进行寄出或者签收快递(数据)的过程。具体时序略。
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
//从 I2C 设备的寄存器读取数据
//参数: I2C句柄;I2C从机地址;寄存器地址;寄存器地址增加大小-I2C_MEMADD_SIZE_8BIT:增加八位,I2C_MEMADD_SIZE_16BIT:增加十六位;数据指针;数据长度;超时时间
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t
MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
//往 I2C 设备的寄存器写入数据
发送 I2C 地址后,发送数据类型再发送数据,其中 数据类型为 0x00 表示控制指令,0x40 表示数据指令。
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
//向某个 I2C 设备传输数据
//参数:I2C 句柄;I2C 从机地址;数据指针;数据长度;超时时间
根据 OLED 的通信方式,发送从机地址后需要在第一个字节中指明之后的数据类型,如果是控制指令
则需要发送 0x00,如果是数据指令则需要发送 0x40。函数原型如下:
/**
* @brief 写数据或者指令到 OLED, 如果使用的是 SPI,请重写这个函数
* @param[in] dat: 要写入的字节
* @param[in] cmd: OLED_CMD 代表写入的字节是指令; OLED_DATA 代表写入的字节是数据
* @retval none
*/
oid oled_write_byte(uint8_t dat, uint8_t cmd)
{
static uint8_t cmd_data[2];
if(cmd == OLED_CMD)
{
cmd_data[0] = 0x00;
}
else
{
cmd_data[0] = 0x40;
}
cmd_data[1] = dat;
HAL_I2C_Master_Transmit(&hi2c2, OLED_I2C_ADDRESS, cmd_data, 2, 10);
}
OLED_init 函数,该函数主要配置 OLED 参数,通过调用 oled_write_byte 传入
OLED_CMD,传输控制指令完成配置
OLED_display_on和OLED_display_off 函数分别是用来关闭OLED 显示和开启OLED
显示
OLED_draw_point 对(x,y)坐标的一个像素点进操作
OLED_draw_line 从(x1,y1)到(x2,y2)的直线经过的像素点进行操作
OLED_show_char 显示一个字符
OLED_show_string 显示一个字符串
OLED_printf Printf 函数功能
OLED_LOGO 显示 Robomaster LOGO
OLED_operate_gram(pen_typedef pen) 函数是操作 OLED_GRAM[128][8]数组的,我们对整个数组进行
操作,操作完成后再通过 OLED_refresh_gram 函数整体刷新 OLED 内部的 GRAM。作用是打开OLED显示.
参数:PEN_WRITE 是将数组都设置为 0xff,对于 OLED 屏幕即为全亮;PEN_CLEAR 是将数组都设置为 0x00,对于 OLED 屏幕即为全灭;PEN_INVERSION 是将数组的值全部反转,通过与 0xff 相减来实现。
OLED_refresh_gram 函数功能是将内部的GRAM[8][128]传输到OLED模块的GRAM,
这样 OLED 就会显示图像。
1. OLED 模块的初始化,调用 OLED_init
2. 通过画图函数,对 stm32 内的 GRAM 数组进行操作
3. 调用 OLED_refresh_gram 函数将 GRAM 数据传输到 OLED 模块的 GRAM 进行显示。
其中2、3步都是OLED_LOGO 函数完成的,内集成画图函数将 GRAM 刷新成 LOGO 的数据,以及最后调用了 OLED_refresh_gram 函数显示。
BMI088 是一种高性能惯性测量单元 (IMU),集成了 16 位 ADC 精度的三轴 螺仪和三轴加速度计。
陀螺仪能测量在三个正交方向上旋转的角速度,也可以用于估算在三个方向上的旋转角度,原理是将旋转的角速度转化为电容的变化即转变为电信号。
加速度计能够测量三个正交方向上的加速度,原理是加速度改变力改变电容改变转化为电信号。
我们实际上是对BMI088中的各种寄存器进行操作。
oid BMI088_read(fp32 gyro[3], fp32 accel[3], fp32 *temperate) BMI088_read(gyro, accel, &temp) //读取角速度、加速度、温度值
SPI通信协议:
SPI 的通信过程如下:
1. 主设备将要进行通讯的从设备的 SS/CS 片选拉低,
2. 主设备通过 SCK 向从设备提供同步通讯所需要的时钟信号
3. 主设备通过 MOSI 向从设备发送 8 位数据,同时通过 MISO 接收从设备发来的 8 位数 据。
4. 通信结束,主设备拉高 SS/CS 片选。
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout) //通过 SPI 进行主机和从机的通信 //参数1:SPI_HandleTypeDef *hspi 即 SPI 的句柄指针,如果是 SPI1 就输入&hspi1,SPI2 就输入&hspi2 参数 2 uint8_t *pTxData 待发送数据的首地址指针 参数 3 uint8_t *pRxData 接收数据的区域的首地址 参数 4 uint16_t Size 待发送的数据长度 参数 5 uint32_t Timeout 最大发送时长
电机
can发送:
具体在CAN_cmd_chassis 函 数 和 CAN_cmd_gimbal 函数中,先将ID、数据位等信息赋值到我们定义的结构体变量中,再在内部调用基于HAL 库的CAN 发送函数 HAL_CAN_AddTXMessag将这些结构体变量的值发送到内部寄存器,就获取了主机要发送给电机的信息。时序啊这些都是HAL库内部的东西,已经封装好了。
CAN_cmd_chassis(int16_t motor1, int16_t motor2, int16_t motor3, int16_t motor4) //CAN_cmd_chassis 函数的输入为电机 1 到电机 4 的驱动电流期望值 motor1 到 motor4, 函数会将期望值拆分成高八位和第八位,放入 8Byte 的 CAN 的数据域中,然后添加 ID (CAN_CHASSIS_ALL_ID 0x200),帧格式,数据长度等信息,形成一个完整的 CAN 数据帧,发送给各个电调。 CAN_cmd_gimbal(int16_t yaw, int16_t pitch, int16_t shoot, int16_t rev) //CAN_cmd_gimbal 函数的的功能为向云台电机和发射机构电机发送控制信号, 输入参数为 yaw 轴电机,pitch 轴电机,发射机构电机的驱动电流期望值 yaw,pitch,shoot(rev 为保留值), 函数会将期望值拆分成高八位和第八位,放入 8Byte 的 CAN 的数据域中,然后添 加 ID(CAN_GIMBAL_ALL_ID 0x1FF),帧格式,数据长度等信息,形成一个完整的 CAN 数据帧,发送给各个电调。 //HAL 库提供了实现 CAN 发送的函数 HAL_CAN_AddTXMessag。CAN_cmd_chassis 函数和AN_cmd_gimbal 函数内部封装了发送函数。 HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox) //将一段数据通过 CAN 总线发送 参数:&hcan1;待发送的 CAN 数据帧信息的结构体指针,包含了 CAN 的 ID,格式等重要信息; 装载了待发送的数据的数组名称;用于存储 CAN 发送所使用的邮箱号
can接收:
CAN 的接收中断函数HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)每当 CAN 完成一帧数据的接收时,就会触发一次 CAN 接收中断处理函数,接收中断函数完成一些寄存器的处理之后会调用 CAN 接收中断回调函数。在中断回调函数中首先判断接收对象的 ID,是否是需要的接收的电调发来的数据。完成判断之后,进行解码,将对应的电机的数据装入电机信息数组 motor_chassiss各个对应的位中。接收中断函数里接收时调用了 HAL 库提供的接收函数 HAL_CAN_GetRxMessage。
HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo, CAN_RxHeaderTypeDef *pHeader, uint8_t aData[]) 接收 CAN 总线上发送来的数据 参数:&hcan1;接收时使用的 CAN 接收 FIFO 号,一般为 CAN_RX_FIFO0; 存储接收到的 CAN 数据帧信息的结构体指针,包含了 CAN 的 ID,格式等重要信息; 存储接收到的数据的数组名称
can总线协议:主机发送信息给can总线上挂的设备(从机),每一个从机有一个对应的ID(地址),每当一个设备发送一帧数据时,总线其他设备会检查这 个 ID 是否是自己需要接收数据的对象,如果是则接收本帧数据,如果不是则忽略。若判断可以接收以后,则发送控制位,规定了本帧数据的长度;再数据位,即 8 个 8 位数据,CAN总线的一个数据帧中所要传输的有效数 据实际上就是这 8Byte......
数据位:如果要发送数据给 1 号到 4 号电调,控制电机的输出电流,从而控制电机转速时,则置标识符(ID)为0x200,则8位数据位分别是4个电调的电流值高八位低八位(4x(1+1)),帧格式和 DLC 也按手册规定置,完成数据发送。
而当要接收电调发送来的数据时,首先根据接收到的 ID 判断究竟接收到的是哪个电调发送来的数据(0x200+电调ID,如电调1则为0x201),再对电调发送来的数据进行解码(数据位的8位分别代表:转子机械角度高低八位,转子转速高低八位,实际转矩电流高低八位,电机温度,null)获取相关信息。
变量声明应写在.c文件里,结构体变量写在.h文件里,.h文件里想用.c声明的变量得加extern
如.h文件里:
typedef struct
{
uint16_t ecd;
int16_t speed_rpm;
int16_t given_current;
uint8_t temperate;
int16_t last_ecd;
} motor_measure_t;//电机数据结构体.c文件里:
motor_measure_t motor_chassis[7];
那么.h文件里:
extern motor_measure_t motor_chassis[7];
舵机
开启定时器
开启定时器的pwm模式
获取遥控器数据指针
设置舵机对应引脚的初电平值(刚开始转过的角度)
当遥控器对应的寄存器值为多少时设置不同的电平,即舵机转过的角度。
PID
//位置式pid
float pidUpdate(PidObject* pid, const float error)
{
float output;
pid->error = error;
if(abs(pid->error)Limit)
{
pid->integ += pid->error; //误差累加,积分项,i
if (pid->integ > pid->iLimit)
{
pid->integ = pid->iLimit;//限幅
}
else if (pid->integ < pid->iLimitLow)
{
pid->integ = pid->iLimitLow;//限幅
}
}
pid->deriv = pid->error - pid->prevError;//此次误差减去上次误差,微分项,d
pid->outP = pid->kp * pid->error;
pid->outI = pid->ki * pid->integ;
pid->outD = pid->kd * pid->deriv;
output = pid->outP + pid->outI + pid->outD;
pid->prevError = pid->error;
return output;
}
void chassis_pid(void)
{
motor.Set_motor_speed[0] = 200;//(rc_ctrl.rc.ch[3]+rc_ctrl.rc.ch[2]+rc_ctrl.rc.ch[0])*3*rc_ctrl.rc.s[0]-spin*98;
motor.Actual_motor_speed[0] = motor_chassis[0].speed_rpm;
motor.Out_motor_speed[0] = pidUpdate(&motor.pid_motor_speed[0] , motor.Set_motor_speed[0] - motor.Actual_motor_speed[0]);
motor.Out_motor_speed[0] = limit_ab(motor.Out_motor_speed[0],-20000,20000);
CAN_cmd_chassis(motor.Out_motor_speed[0],motor.Out_motor_speed[1],motor.Out_motor_speed[2],motor.Out_motor_speed[3]);
}
主函数中 :
init_pid_all();//pid初始化
while(1)
{
chassis_pid();//不断更新pid并给电机发送数据
HAL_Delay(10);
}
//pid结构体
typedef struct
{
float error;
float integ;
float iLimit;
float iLimitLow;
float Limit;
float deriv;
float outP;
float outI;
float outD;
float kp;
float ki;
float kd;
float prevError;
} PidObject;
//电机结构体
typedef struct
{
s16 Out_Car_turn;
s16 Set_motor_speed[8];
s16 Actual_motor_speed[8];
s16 Out_motor_speed[8];
s16 Out_Claw_Round[2];
s16 Actual_Claw_round[2];
PidObject pid_motor_speed[8];//电机转速的结构体变量
PidObject pid_car_turn;
PidObject pid_claw_round[2];
PidObject pid_x_speed;
PidObject pid_z_speed;
PidObject pid_motor[4];
} Motor;
十六. IMU 温度控制
使用 PID 控制算法对 IMU 进行温度控制
零漂现象是指当物理量输入为零,传感器测量的输出量不为零的现象。即 IMU 没有任何运动,陀螺仪和加速度计(这俩都是传感器)也会读取到一定大小的数据,并将其当作是由 IMU 运动产生的。
PID:() = ∗ () + ∗ ∫ () + ∗ ()/
err(t)即误差值,Kp,Ki,Kd 分别为比例,积分,微分三项的系数。
单片机系统是离散的,所以位置式pid公式:() = ∗ () + ∗ ∑(i=0到k)() + ∗ [() − ( − 1)];
增量式pid公式:() = ∗ [() − ( − 1)] + ∗ () + ∗ [() − 2 ∗ ( − 1) + ( − 2)]
实际上也可以理解为我们在t0、t1、t2...每隔相等的时间(如1s)进行一次采样,计算这一次输出量的值和上一次的值作为误差。
pid算法中,以比例环节为例,控制器=k*eu,而非最终得到的输出量=k*eu。例如我们给水壶加热,那么最终需要的量是温度,但是pid控制的是加热功率:如我们要加热到80度,现在75度,则eu=5,加热功率就是5k,然后温度上升;当加热功率小于散热的时候温度下降,通过这种改变加热功率的方式才是pid调节。
以我们的车子追踪一个会动的物体为例:error为车子和物体之间的距离。比例算法中,由于物体在动,距离增大,那么error增大,控制车子速度的量增大,某一瞬间to车速等于物体速度,设此时距离为x0;此后车子和物体速度相同,error不变,那么它们将会永远间隔这样一个距离,称为稳态误差。
引入积分算法后,控制车子速度的量不仅是error乘上比例系数k,还有error对时间的积分(乘以积分系数)。t0时刻车速等于物体速度,由于时间永远在增加,所以积分项一直增加,车子速度肯定会增加,那么车子将继续追赶物体;此后车子与物体的距离减小,那么比例项的作用减弱,而积分项的作用一直增加,直到某一刻它们的作用扯平使得车子速度又等于物体速度(所以车子速度在第一次相等后经历了增加再减小的过程);然后发现由于时间增加积分项又增加,比例项又减小,无线逼近。
若振荡(车子超过物体了),采用微分控制。控制变化曲线的斜率。
十七 底盘控制任务
主文件:
can_filter_init();
init_pid_all();//pid的ki、kp、kd等参数初始化
remote_control_init();
RC_ctrl_t * local_rc_ctrl = get_remote_control_point();
while(1)
{
chassis_pid();//pid参数更新,并将更新的数据(电流值、转速等)发送
HAL_Delay(10);
}
pid.c文件:
float pidUpdate(PidObject* pid, const float error)
//pid参数更新函数,输入变量为pid结构体和error
{
error赋值到pid结构体内部的error中去
误差累加得到积分项
对积分项进行限幅
德塔误差=上次误差-这次误差,得到微分项
pid->outP = pid->kp * pid->error;
pid->outI = pid->ki * pid->integ;
pid->outD = pid->kd * pid->deriv;
output = pid->outP + pid->outI + pid->outD;//更新pid参数并返回输出值
上次误差=这次误差
返回输出值
}
void chassis_pid(void)
//调用更新的pid参数输出值,并发送到电机
{
vx=local_rc_ctrl->rc.ch[0]/6.6*50;//车速与遥控器成正比
vy=-local_rc_ctrl->rc.ch[1]/6.6*50;
wz=-local_rc_ctrl->rc.ch[2]/6.6*50;
v1=-vx-vy-wz;
v2=vx-vy+wz;
v3=vx+vy-wz;
v4=-vx+vy+wz;
/* 底盘四个电机的PID计算并输出 */
//电机1:
motor.Set_motor_speed[0] = v1;
motor.Actual_motor_speed[0] = motor_chassis[0].speed_rpm;
motor.Out_motor_speed[0] = pidUpdate(&motor.pid_motor_speed[0] ,
motor.Set_motor_speed[0] - motor.Actual_motor_speed[0]);
//更新后的电流值赋值给motor.Out_motor_speed变量,输入是转速,输出motor.Out_motor_speed变量是电流值。
motor.Out_motor_speed[0] = limit_ab(motor.Out_motor_speed[num],-10000,10000);
//对电流值进行限幅
CAN_cmd_chassis(motor.Out_motor_speed[0],motor.Out_motor_speed[1],
motor.Out_motor_speed[2],motor.Out_motor_speed[3]);
//将得到的电流值发送给各个电机
}