STM32HAL库学习笔记
目录
定时器
一些小细节
输入捕获计算信号频率
输入捕获计算占空比与频率
使用定时器不改变占空比的同时改变频率的方法
串口
重定向原理
重定向代码
怎么从串口接收到的字符串数据中解析出float型的数据
strchr
sscanf
memset
第一种实现方法
RTC实时时钟
LCD显示
%s占位符与%c占位符的区别
I2C读写操作
定时器
一些小细节
其中HAL_TIM_Base_Start和HAL_TIM_Base_Start_IT这两个函数是不能够同时调用的,
-
HAL_TIM_Base_Start()
:仅启动定时器,不涉及中断-
HAL_TIM_Base_Start_IT()
:启动定时器并开启更新中断- 不能同时调用:二者会冲突操作硬件寄存器和 HAL 库状态机
- 正确做法:根据需求选择其中一个函数,或通过
HAL_TIM_ENABLE_IT()
动态管理中断
输入捕获计算信号频率
仅仅用于信号频率的检测就只需要使用定时器的上升沿触发就可以了 , 捕获两个高电平的之间时间 ,用定时器时钟比上时间差就能得到频率 , 需要注意的细节在于+1的操作 , 因为计数都是从零开始的
下面是定时器17的输入捕获配置
//主函数里面打开定时器17的输入捕获中断
HAL_TIM_IC_Start_IT(&htim17,TIM_CHANNEL_1);//编写中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM17){//测量频率就是定时器的时钟比上上升沿计数值capture = HAL_TIM_ReadCapturedValue(&htim17 ,TIM_CHANNEL_1);TIM17->CNT = 0;fre_out = main_clock/(capture*((TIM17->PSC)+1));}
}
输入捕获计算占空比与频率
使用定时器3 , combined mode的PWM input on CH1实现
通道2配置为直接模式 , 通道2配置为间接模式(硬件自动配置) , 通道1和通道2分别捕获上升沿和下降沿 , 设置滤波系数都为3.
配置定时器预分频系数为80(从0开始计数) , 主频80M , 所以定时器时钟为1M , 频率的计算就是定时器时钟/上升沿到上升沿的时间
//主函数中打开定时器三的输入捕获中断,两个通道都需要打开
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);//编写中断函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM3){//如果是定时器3的中断period = TIM3->CCR1+1;//硬件自动捕获周期上升沿到上升沿high_time = TIM3->CCR2+1;//硬件自动捕获高电平时间 , 上升沿到下降沿fre_cap = main_clock / (period * (TIM3->PSC + 1));//直接计算频率duty = ((float)high_time / period) * 100;//计算占空比}
}
发现的hal库小细节
//HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1 | TIM_CHANNEL_2);
//同时打开定时器三的中断是不行的 , 需要像下面这样一个通道一个通道的打开才能正常进入中断HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);
使用定时器不改变占空比的同时改变频率的方法
思路就是直接在定时器中改变定时器的预分频系数 , 这样占空比就能保持不变 , 但是频率会逐渐改变
好像使用__HAL_TIM_SET_PRESCALER(&htim15 ,prescaler);这个函数在中断中去进行改变定时器的预分频系数总是会卡死的 , 改成直接操作寄存器的方式似乎就可以了TIM->PSC = prescaler
定时器中断中不要改变其他定时器的预分频系数 ,不然会程序会直接卡死,测试了几个其他的函数发现只有setprescaler会卡死 , 然后加上先失能全局中断后使能全局中断之后就正常了 , 说明可能修改的定时器的中断函数正在运行 , 但是预分频系数突然被改变了导致的,所以需要先关闭全局中断 , 修改完成之后再使能全局中断 , 也可以直接操作寄存器实现预分频系数的修改 .
也就是加上这两句 , 更加具体的内容可以问Ai__disable_irq();//失能全局中断
.........
__enable_irq();//使能全局中断
下面是代码 , 还有一些细节需要注意的就是ARR , CCR , PSC都是从零开始的所以需要仔细的调节一下
//按键部分
else if(short_press == 2){//选择if(jiemian_mode == 0){//如果是数据界面__HAL_TIM_ENABLE_IT(&htim15, TIM_IT_UPDATE);//重新使能更新中断
}//实现定时器的使能
就是手动开启定时器的中断 , 中断中关闭定时器的中断//定时器就15实现上升或者下降沿
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//中断实际上是只能有一个的
{//如果是定时器15的中断if(htim->Instance == TIM15){if(fre_mode == 0){//如果现在是低频,需要缓慢变化到高频if(TIM2->PSC >= 40){//40进来之后-最后一次//__disable_irq();//失能所有中断TIM2->PSC-=1;if(TIM2->PSC <= 39){//这里也应该是减1的值fre_mode =1;__HAL_TIM_DISABLE_IT(&htim15,TIM_IT_UPDATE);//完成之后关闭更新中断}//__enable_irq();//使能所有中断}}else if(fre_mode == 1){//如果现在是高频需要变化到低频if(TIM2->PSC <= 78){//78进来再+最后一次//__disable_irq();//失能所有中断TIM2->PSC+=1;if(TIM2->PSC >= 79){//应该是-1的值也就是80-1的值fre_mode =0;__HAL_TIM_DISABLE_IT(&htim15,TIM_IT_UPDATE);//完成之后关闭更新中断}//__enable_irq();//使能所有中断}}}
}
串口
重定向原理
就是更改了printf与scanf的底层函数,从另外的地址进行读写操作
重定向代码
可以直接套用 ,模式配置成最异步最简单的就可以
这个是普通的阻塞发送的串口重定向
/*--------------------串口重定向------------------*/
int fputc(int ch,FILE *f)
{
//采用轮询方式发送1字节数据,超时时间设置为无限等待HAL_UART_Transmit(&huart2,(uint8_t *)&ch,1,HAL_MAX_DELAY);return ch;
}int fgetc(FILE *f)
{uint8_t ch;// 采用轮询方式接收 1字节数据,超时时间设置为无限等待HAL_UART_Receive( &huart2,(uint8_t*)&ch,1, HAL_MAX_DELAY);return ch;
}
使用串口中断进行数据接受的时候使用会使用到
HAL_UART_Receive_IT(&huart2,(uint8_t *)rec_data,num_byte);
这个函数会使能串口中断接受然后我们在串口接受回调函数中接受数据 ,需要注意的点在于这个函数只要接收到1个字节就会触发中断,所以需要进行逐个字节的解析,这是比较麻烦的下面怎么利用串口的空闲中断和DMA进行数据传输
配置界面如下
//主函数中
HAL_UARTEx_ReceiveToIdle_DMA(&huart1 , rx_buffer , sizeof(rx_buffer));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx , DMA_IT_HT);
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if(huart->Instance == USART1){//如果是串口1的中断HAL_UARTEx_ReceiveToIdle_DMA(&huart1 , rx_buffer ,sizeof(rx_buffer));sscanf(rx_buffer ,"%d-%d",&a,&b);//读取特定字符,强大好使printf("%d-%d\n",a,b);if(rx_buffer[0]=='1'&&rx_buffer[1]=='2'&&rx_buffer[2]=='3'&&rx_buffer[3]=='\n'){light(1,1);}else if(rx_buffer[0]=='3'&&rx_buffer[1]=='2'&&rx_buffer[2]=='1'&&rx_buffer[3]=='\n'){light(1,0);}}__HAL_DMA_DISABLE_IT(&hdma_usart1_rx , DMA_IT_HT);memset(rx_buffer,0,sizeof(rx_buffer));
}void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){//错误中断HAL_UARTEx_ReceiveToIdle_DMA(&huart1 , rx_buffer ,sizeof(rx_buffer));__HAL_DMA_DISABLE_IT(&hdma_usart1_rx , DMA_IT_HT);memset(rx_buffer,0,sizeof(rx_buffer));}
}
怎么从串口接收到的字符串数据中解析出float型的数据
其实不只是浮点数像整数型等等都是可以的
从串口接收到的字符数据中解析出我们想要的数据总共有三种方法 , 分别是使用ssacnf函数,使用atof函数以及纯手动进行数据解析 ,我将讲解其中的第一和第二种
首先来讲讲C语言中的字符串处理函数
strchr
char *strchr(const char *str, int c)
这个函数用于查找字符串中我们想要的字符 , 其中str是给定字符串 , 其中c这个字符是在str中要搜索的字符串 ,返回值是指向该字符串中第一次出现的字符的指针,不包含时返回空指针
sscanf
sscanf()这个函数与我们常见的scanf函数的区别主要是sscanf()函数从给定的字符串中读取指定字符,而scanf函数从屏幕中读取指定字符 ,他们的函数原型分别如下
Int sscanf( string str, string fmt, mixed var1, mixed var2 ... );//str就是给定字符串,fmt是我们要的格式 ,var是我们指定的格式字符串保存的变量
int scanf( const char *format [,argument]... );//这个不过多讲了
下面的资料来自于百度百科 , 这个函数真的超级强大 ,才注意到
1、一般用法
char buf[512] = ;
sscanf("123456 ", "%s", buf);
printf("%s\n", buf);
结果为:123456
2. 取指定长度的字符串。如在下例中,取最大长度为4字节的字符串。
sscanf("123456 ", "%4s", buf);
printf("%s\n", buf);
结果为:1234
3. 取到指定字符为止的字符串。如在下例中,取遇到空格为止字符串。
sscanf("123456 abcdedf", "%[^ ]", buf);
printf("%s\n", buf);
结果为:123456
4. 取仅包含指定字符集的字符串。如在下例中,取仅包含1到9和小写字母的字符串。
sscanf("123456abcdedfBCDEF", "%[1-9a-z]", buf);
printf("%s\n", buf);
结果为:123456abcdedf
5. 取到指定字符集为止的字符串。如在下例中,取遇到大写字母为止的字符串。
sscanf("123456abcdedfBCDEF", "%[^A-Z]", buf);
printf("%s\n", buf);
结果为:123456abcdedf
6.给定一个字符串iios/12DDWDFF@122,获取 / 和 @ 之间的字符串,先将 "iios/"过滤掉,再将非'@'的一串内容送到buf中
sscanf("iios/12DDWDFF@122", "%*[^/]/%[^@]", buf);
printf("%s\n", buf);
结果为:12DDWDFF
7.给定一个字符串"hello, world",仅保留"world"。(注意:“,”之后有一空格)
sscanf("hello, world", "%*s%s", buf);
printf("%s\n", buf);
结果为:world P.S. %*s表示第一个匹配到的%s被过滤掉,即hello,被过滤了,如果没有空格则结果为NULL
memset
memset是C和C++中的内存初始化函数,可以将指定内存区域设置为特定值
void *memset(void *s, int ch, size_t n);//函数原型
void *s是要填充的内存区域的起始地址
int ch指定了要填充到内存的值(实际填充时ch 会被转换为unsigned char 类型)
size_t n 决定了要填充的字节数
第一种实现方法
使用时记得在keil中勾选use micro lib这个选项
strchr+sscanf实现
#include <stdio.h> // 确保标准库支持char rx_data[] = "k0.2\n";
float value = 0;// 查找'k'的位置并解析
char *key_ptr = strchr(rx_data, 'k');
if (key_ptr != NULL) {if (sscanf(key_ptr + 1, "%f", &value) == 1) { // +1跳过'k'// 成功解析,value = 0.2} else {// 解析失败处理}
}
uint8_t rx_buffer[128];//接受缓存区
uint8_t rx_index = 0;
_Bool rx_complete = 0;
float new_k = 0;
//在这里我们发送的数据是k0.4\n,总共是5个字符
//中断进行数据解析
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if (huart->Instance == USART2){HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buffer, 128); //接收完毕后重启串口DMA模式接收数据//数据处理,从字符串中解析数据变成0.4,使用字符函数实现if(rx_buffer[0] == 'k'&& rx_buffer[4] == '\n')//进行{//其中target是指向字符串中第一次出现我们想要字符的指针char* target = strchr((const char *)rx_buffer , 'k');if(target != NULL){if(sscanf((const char*)target + 1,"%f",&new_k) == 1)//解析成功的处理{light(3,1);} else {//解析失败的处理light(4,1);}}}//HAL_UART_Transmit(&huart2, rx_buffer, Size, 0xffff); // 将接收到的数据再发出__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); // 手动关闭DMA_IT_HT中断memset(rx_buffer, 0, 128); // 清除接收缓存}
}
第二种
使用atof函数
RTC实时时钟
RTC实时时钟就是32内部的一个独立的定时器 ,如果使用外部低速晶振那就是32.768khz,进行预分频,如果是127和255,就是32768/((127+1)*(255+1)) = 1Hz
即便不需要获取日期,也需要添加获取日期的函数,不然时间是不会流动的
RTC_TimeTypeDef sTime = {0};RTC_DateTypeDef sDate = {0};RTC_AlarmTypeDef sAlarm = {0};//在上面先定义结构体HAL_RTC_GetTime(&hrtc,&sTime,RTC_FORMAT_BIN);//获取时间保存到Time结构体HAL_RTC_GetDate(&hrtc,&sDate,RTC_FORMAT_BIN);//获取日期保存到Date结构体
如果想要手动修改RTC闹钟的时间可以自己再写一个函数
void Set_RTC_Alarm(uint8_t hours ,uint8_t minutes ,uint8_t seconds)
{RTC_AlarmTypeDef sAlarm = {0};HAL_RTC_DeactivateAlarm(&hrtc, RTC_ALARM_A);//弃用旧闹钟//实际测试中好像不需要这两句代码也能正常修改,不知道为什么__HAL_RCC_PWR_CLK_ENABLE();//解除备份域时钟保护HAL_PWR_EnableBkUpAccess();//使能备份域时钟操作sAlarm.AlarmTime.Hours = hours;sAlarm.AlarmTime.Minutes = minutes;sAlarm.AlarmTime.Seconds = seconds;sAlarm.AlarmTime.SubSeconds = 0;sAlarm.AlarmTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;//这个接口是弃用的sAlarm.AlarmTime.StoreOperation = RTC_STOREOPERATION_RESET;sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY;sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_NONE;sAlarm.AlarmDateWeekDaySel = RTC_ALARMSUBSECONDMASK_NONE;sAlarm.AlarmDateWeekDay = 1;sAlarm.Alarm = RTC_ALARM_A;HAL_RTC_SetAlarm_IT(&hrtc , &sAlarm,RTC_FORMAT_BIN);HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0);HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
}
LCD显示
主要点在于通过定时器定时中断设置标志为进行闪烁显示,在中断里面写一个标志位 ,不断的翻转这个标志位的状态实现,不同的状态显示不同的内容就能实现闪烁的显示 , 或者下划线什么的 ,这个比较简单了
显示一些不常见字符的方法 例如显示百分号的方法
char baifen = '%';
sprintf(showstring," P:%.2f%c ",duty,baifen);
LCD_DisplayStringLine(Line4,(uint8_t *)showstring);
//这样就能显示百分号了
%s占位符与%c占位符的区别
特性 | %c | %s |
---|---|---|
操作对象 | 单个字符 (char ) | 字符串(字符数组/指针) |
内存操作 | 1字节 | 连续内存直到 \0 |
输入行为 | 读取任何字符(包括空格) | 跳过空白符,读到空格停止 |
是否需要 & | 需要(scanf 中) | 不需要(数组名即地址) |
输出终止条件 | 无(固定1字符) | 遇到 \0 停止 |
C语言中 , 不要直接写3/8这样你会得到0 , 想要得到浮点数就写3.0/8.0这样计算得到浮点数
I2C读写操作
实际上就是E2PROM的读写操作的实现应当是较为简单的 , 下面给出代码
//0xa0是写地址
//0xa1是读地址
void eeprom_write(uint8_t addr ,uint8_t dat)
{I2CStart();I2CSendByte(AT24C02_Adress);//写哪个从机I2CWaitAck();//等待应答I2CSendByte(addr);//发送要写地址I2CWaitAck();I2CSendByte(dat);//发送要写的数据I2CWaitAck();I2CStop();HAL_Delay(20);//防止连续写入时出错
}uint8_t eeprom_read(uint8_t addr)
{I2CStart();I2CSendByte(AT24C02_Adress);//写哪个从机I2CWaitAck();//等待应答I2CSendByte(addr);//发送要写地址I2CWaitAck();//等待应答I2CStop();I2CStart();//重新开始是为了将控制权交给从机I2CSendByte(0xa1);//发送要读的地址I2CWaitAck();//等待应答uint8_t data = I2CReceiveByte();//接受数据I2CSendNotAck();//发送不回应,也就是不继续读了I2CStop();//停止return data;
}