写在前面
包含全部模块和lcd高亮和翻转、ADC_DMA等特殊操作。
新建工程
选择new project
选择正确的型号
RCC配置
SYS中debug配置
时钟配置
project设置,其中名称、路径不能带有空格或者中文,否则会生成工程失败。软件包选择下载好的比赛提供的版本。
勾选
之后点击generate code生成工程,打开工程,debug选项更改为板载调试器
C/C++选项添加文件头文件目录,这里新建一个文件夹bsp用来放置个人的文件
utilities中点击setting,选择合适的下载算法
随后先建一个组,更名为bsp,之后个人的文件添加进这个组中
新建两个文件并保存为my_main.c和my_main.h,保存在bsp文件夹中,右键bsp,添加已经存在的文件,将my_main.c添加 。之前学习时将每个模块分别建立.c和.h文件,但是在不同的模块中使用同一个函数时会出现交叉引用和声明,非常容易出现重复定义和缺少声明,而且每次都要在.h文件中写重复的代码,在main.c中包含头文件和书写初始化函数,比较繁琐和出现问题,所以另建一个个人代码文件,代码都在此。
点击扳手,将字体格式更改为UTF-8,否则中文可能会出现乱码
个人代码要在begin end中间,否则cubemx重新生成代码后会删除,在main.h里定义uint和uchar。代码编程时注意遵循C语言的模块化编程和基本语法和逻辑规则。
在my_main.h和my_main.c中分别添加以下代码
#ifndef _MY_MAIN_H_
#define _MY_MAIN_H_
#include "main.h"
void setup(void);
void loop(void);
#endif
#include "my_main.h"
void setup(void)
{
}
void loop(void)
{
}
main.c中添加头文件和setup,loop函数
LED
根据原理图,LED用到PC8到PC15,左侧引脚可以看做芯片直接和右侧引脚连通,引脚全部设置为output,左侧高电平,右侧低电平的话LED导通,此处使用前四个LED。PD2高电平使能,低电平关闭。设置为关闭。
my_main.h代码
#include "stdbool.h"
void LED_disp(uint lednumb,bool on_off);
led.c代码
void LED_disp(uint lednumb,bool on_off)
{
static uint8_t mmry=0x00;
if(on_off==1) mmry|=lednumb;
if(on_off==0) mmry&=~lednumb;
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);//GPIO_PIN_All,只有A大写
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);//打开锁存器PD2
HAL_GPIO_WritePin(GPIOC,mmry<<8,GPIO_PIN_RESET);//mmry左移八位到要控制的高八位
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);//关闭锁存器PD2
}
LED的控制是前一个参数控制灯的编号,具体为十六进制参数转化为二进制后为1的位置,例如LED_disp(0x80,1)的0x80转化为二进制为1000,则控制的是第四位。1是亮,0是灭。闪烁可以通过systick的不断改变flag位来实现,如LED_disp(0x80,flag)。
注意:LED会有残留,不用时需要写清零灭灯语句
KEY
按键有短按长按双击。按键按下,相连引脚低电平,未按下低高电平。用到的按键引脚改为GPIOINPUT,GPIO中把上下拉模式中选择上拉pull up。
定时器配置,随便找一个TIM4,clock source选择internal clock内部时钟。下面的参数,分频系数psc,自动重装载值ARR,定时器工作频率=外部总线频率/(PSC+1)/(ARR+1),这样就可知每次按键按下的判断一次的时间,可以根据改变按键按下时间的计数times来改变消抖和长按的判别时间。NVIC设置里面enabled中断使能勾选。最后生成工程。
在setup函数中开启定时器中断
HAL_TIM_Base_Start_IT(&htim4);
.h代码
#include "tim.h"
实现代码,其中void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)是定时器中断回调函数,每次定时器产生中断都会进入一次,下面if语句判断中断属于定时器4时,会执行其中内容。
typedef struct keys
{
bool status;
bool shortdown;
bool longdown;
uint8_t times;
}KEY;
KEY key[4];
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM4)
{
key[0].status=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].status=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].status=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].status=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
}
for(int i=0;i<4;i++)
{
if(key[i].status==0)
{
key[i].times++;
}
if(key[i].status==1)
{
if(key[i].times>40)key[i].longdown=1;
else if((key[i].times>2)&&(key[i].times<40))key[i].shortdown=1;
key[i].times=0;
}
}
}
key[i].times=0清零要在给出长短按的判断之后,且不能是在for循环之中,否则会一直清零。
如果不记得中断回调函数如何写,可以在如下文件的靠近最下端处找到。
LCD
lcd和led有共用的io口,led和lcd的刷新函数要在同一个顺序执行函数中,不能一个在while1中,另一个在中断中。
不需要cubemx配置,将fotns.h和lcd.h和lcd.c添加到个人工程文件夹bsp,lcd.c加入到工程。
my_main.h代码
#include "lcd.h"
#include "stdio.h"
void lcd_proce(void);
mylcd.c代码
bool view=0;
void lcd_proce(void)
{
if(view==0)
{
char text[30];
sprintf(text," 0 ");
LCD_DisplayStringLine(Line3,(uint8_t*)text);
}
if(view==1)
{
char text[30];
sprintf(text," 1 ");
LCD_DisplayStringLine(Line3,(uint8_t*)text);
}
}
在setup中初始化,清屏,设置背景色,设置文本色。显示背景色和前景色就是backcolor和textcolor,用LCD_SetBackColor()LCD_SetTextColor();来实现。选中括号中的颜色后,比如设置的是Black,右键选择Go to Definition Of ‘Black’,能看到定义的其他颜色可填。
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
在官方例程里面main.c就是官方的写法例子,在LCD_Init()后就是LCD使用的具体语法。
定义一个数组紧接着写中括号并且加上数字表示数组长度,sprintf(第一个参数text就是定义的数组,”第二个参数写自己想要打印的内容”),打印的内容后面可跟一个数据,则需要加第三个参数并且指名所跟数据的类型和名称,使用sprintf函数显示百分号%时输入%%即可。如
char text[30];
sprintf(text," PA1 ");
LCD_DisplayStringLine(Line3,(uint8_t*)text);
sprintf(text," F:%dHz ",pa1_frq);
LCD_DisplayStringLine(Line4,(uint8_t*)text);
sprintf(text," D:%d%% ",pa1_duty);
LCD_DisplayStringLine(Line5,(uint8_t*)text);
能选择的行数Line为0到9一共十行。
高亮显示
如果需要高亮显示,在写sprintf和LCD_DisplayStringLine之前写一下需要高亮的颜色的背景色设置语句,以设置line0为黄色高亮显示为例,其他行不变。
LCD_SetBackColor(Yellow);
Sprintf(text,"numbe",1);
LCD_DisplayStringLine(Line0,(uint8_t *)text,);
LCD_SetBackColor(Black);
Sprintf(text,"numbe",1);
LCD_DisplayStringLine(Line0,(uint8_t *)text,);
lcd翻转显示
在所给的lcd液晶控制器资料中手册中,搜索diretion能够找到对于显示方向的描述SS位控制横向扫描方向,GS位控制纵向扫描方向,搜索GS和SS,能看到分别所在的寄存器位置
搜索60h和01h,能找到GS和SS设置。这里的60和01都是16进制,对应1和96号寄存器。
lcd.c中将函数void REG_932X_Init(void)复制粘贴到下方,更名为void REG_932X_Init1(void),其中1和96号寄存器对应的SS和GS的值更改即可,此时看到代码注释给了需要更改的值,更改即可。
void REG_932X_Init1(void)
{
LCD_WriteReg(R227, 0x3008); // Set internal timing
LCD_WriteReg(R231, 0x0012); // Set internal timing
LCD_WriteReg(R239, 0x1231); // Set internal timing
LCD_WriteReg(R1, 0x0100); // set SS and SM bit //0x0100
LCD_WriteReg(R2, 0x0700); // set 1 line inversion
LCD_WriteReg(R3, 0x1030); // set GRAM write direction and BGR=1.
LCD_WriteReg(R4, 0x0000); // Resize register
LCD_WriteReg(R8, 0x0207); // set the back porch and front porch
LCD_WriteReg(R9, 0x0000); // set non-display area refresh cycle ISC[3:0]
LCD_WriteReg(R10, 0x0000); // FMARK function
LCD_WriteReg(R12, 0x0000); // RGB interface setting
LCD_WriteReg(R13, 0x0000); // Frame marker Position
LCD_WriteReg(R15, 0x0000); // RGB interface polarity
/**************Power On sequence ****************/
LCD_WriteReg(R16, 0x0000); // SAP, BT[3:0], AP, DSTB, SLP, STB
LCD_WriteReg(R17, 0x0007); // DC1[2:0], DC0[2:0], VC[2:0]
LCD_WriteReg(R18, 0x0000); // VREG1OUT voltage
LCD_WriteReg(R19, 0x0000); // VDV[4:0] for VCOM amplitude
// Delay_Ms(200); // Delay 200 MS , Dis-charge capacitor power voltage
HAL_Delay(200);
LCD_WriteReg(R16, 0x1690); // SAP, BT[3:0], AP, DSTB, SLP, STB
LCD_WriteReg(R17, 0x0227); // R11H=0x0221 at VCI=3.3V, DC1[2:0], DC0[2:0], VC[2:0]
// Delay_Ms(50); // Delay 50ms
HAL_Delay(50);
LCD_WriteReg(R18, 0x001D); // External reference voltage= Vci;
// Delay_Ms(50); // Delay 50ms
HAL_Delay(50);
LCD_WriteReg(R19, 0x0800); // R13H=1D00 when R12H=009D;VDV[4:0] for VCOM amplitude
LCD_WriteReg(R41, 0x0014); // R29H=0013 when R12H=009D;VCM[5:0] for VCOMH
LCD_WriteReg(R43, 0x000B); // Frame Rate = 96Hz
// Delay_Ms(50); // Delay 50ms
HAL_Delay(50);
LCD_WriteReg(R32, 0x0000); // GRAM horizontal Address
LCD_WriteReg(R33, 0x0000); // GRAM Vertical Address
/* ----------- Adjust the Gamma Curve ---------- */
LCD_WriteReg(R48, 0x0007);
LCD_WriteReg(R49, 0x0707);
LCD_WriteReg(R50, 0x0006);
LCD_WriteReg(R53, 0x0704);
LCD_WriteReg(R54, 0x1F04);
LCD_WriteReg(R55, 0x0004);
LCD_WriteReg(R56, 0x0000);
LCD_WriteReg(R57, 0x0706);
LCD_WriteReg(R60, 0x0701);
LCD_WriteReg(R61, 0x000F);
/* ------------------ Set GRAM area --------------- */
LCD_WriteReg(R80, 0x0000); // Horizontal GRAM Start Address
LCD_WriteReg(R81, 0x00EF); // Horizontal GRAM End Address
LCD_WriteReg(R82, 0x0000); // Vertical GRAM Start Address
LCD_WriteReg(R83, 0x013F); // Vertical GRAM Start Address
LCD_WriteReg(R96, 0xA700); // Gate Scan Line 0xA700
LCD_WriteReg(R97, 0x0001); // NDL,VLE, REV
LCD_WriteReg(R106, 0x0000); // set scrolling line
/* -------------- Partial Display Control --------- */
LCD_WriteReg(R128, 0x0000);
LCD_WriteReg(R129, 0x0000);
LCD_WriteReg(R130, 0x0000);
LCD_WriteReg(R131, 0x0000);
LCD_WriteReg(R132, 0x0000);
LCD_WriteReg(R133, 0x0000);
/* -------------- Panel Control ------------------- */
LCD_WriteReg(R144, 0x0010);
LCD_WriteReg(R146, 0x0000);
LCD_WriteReg(R147, 0x0003);
LCD_WriteReg(R149, 0x0110);
LCD_WriteReg(R151, 0x0000);
LCD_WriteReg(R152, 0x0000);
/* Set GRAM write direction and BGR = 1 */
/* I/D=01 (Horizontal : increment, Vertical : decrement) */
/* AM=1 (address is updated in vertical writing direction) */
LCD_WriteReg(R3, 0x01018); //0x1018
LCD_WriteReg(R7, 0x0173); // 262K color and display ON
}
随后在lcd.h中将新的函数声明。
void REG_932X_Init1(void);
通过搜索发现此函数是在lcd初始化函数中调用的,所以在需要反转显示的位置前调用LCD_Clear(Black)清屏后调用新的修改后的函数void REG_932X_Init1(void)。
PWM
以PA6和PA7为例,引脚配置为CH1通道,CH1N为互补PWM波,不选,选择定时器和对应的CH1的模式,以TIM16为例,TIM17同理。预分频(psc):CPU运行频率先经过它分频再进入计时器,如CPU运行在 x Mhz 下,预分频为 y,那进入计时器的频率也就为 x/(y+1) Mhz(因为从0计数,所以是y+1)。自动重装值(arr):指一次周期的计数长度。脉冲长度(pulse):指输出脉冲的计数长度。
频率=外部总线频率/(PSC+1)/(ARR+1),占空比为pulse/(ARR+1),PSC,ARR,pulse在代码中可以重新设置和调整:
/*
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,180); //修改CCR寄存器
__HAL_TIM_SET_AUTORELOAD(&htim2,1440); //修改ARR寄存器,没有通道限制,整个定时器一起改
__HAL_TIM_SET_PRESCALER(TIM_TypeDef* TIMx, uint32_t Prescaler);//修改预分频系数
举一个例子:
__HAL_TIM_SET_PRESCALER(&htim2, 1000);
这条语句就是把定时器二的预分频系数设为1000
*/
my_main.h代码
void pwm_proce(void);
my_main.c代码
int pa6_duty=200;
int pa7_duty=100;
void pwm_proce(void)
{
__HAL_TIM_SetCompare(&htim16,TIM_CHANNEL_1,pa6_duty);
__HAL_TIM_SetCompare(&htim16,TIM_CHANNEL_1,pa7_duty);
}
setup中开启
HAL_TIM_PWM_Start(&htim16,TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim17,TIM_CHANNEL_1);
输入捕获频率和占空比
可用CH1和CH2通道的引脚(主要)
通过跳线帽连接到PA15和PB4引脚,选择CH1通道,只有ch1和ch2可以从模式清除,这里选择了TIM2和TIM3的CH1。
TI1FP1对上升沿敏感,TI1FP2对下降沿敏感,CNT计数器在80MHZ经过分频后的频率下进行计数(比如分频80,则一微秒计时一次),而ccr1和ccr2则会在上升和下降沿来临时记录下cnt的值,cnt通过从模式在上升沿来临时,ccr1存下数据后归位。由此ccr1就是一个周期的计数,ccr2就是高电平时间的计数。 输入到从模式控制器的信号只能是TI1FP1和TI1FP2,所以只能用CH1和CH2通道
tim2配置,tim3同理
从模式触发源:所连引脚是通道一就是TI1FP1,通道二就是TI2FP2。
channel1和2的直接模式和间接模式:所用的引脚直接相连的通道号为直接模式,如果引脚是channel2,则channel2为直接模式,否则间接模式占用直接相连的通道,直接模式会另开一个引脚。
channel1和2的上升沿和下降沿:直接通道是上升沿,间接通道是下降沿。
.h
void in_pro(void);
setup中开启IC输入捕获(频率和占空比分别是上升和下降的通道,防止开错或者忘开,都开启)
HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_1);
HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_2);
HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_1);
HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_2);
.c
float frq1=0,frq2=0;
float duty1=0,duty2=0;
void in_pro(void)
{
frq1=1000000.0f/(HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_1)+1);
duty1=(HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_2)+1)*100.0f/(HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_1)+1);
frq2=1000000.0f/(HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_1)+1);
duty2=(HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_2)+1)*100.0f/(HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_1)+1);
}
不可用CH1和CH2的引脚(备用)
和能使用从模式清零的方式相比,此方式通用性更好,有的引脚不能使用CH1和CH2,区别在于
1.需首先开启的输入捕获是在中断方式下,函数多了"IT"。
2.判断中断的计时器号,然后判断htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1
3.此方式在读取两个通道的寄存器的值后需要手动归零计时器并且重新打开输入捕获的中断模式。Clock Source选择Integral Clock
以TIM2_CH1为例
Channel1选择 Input Capture direct mode
对方波的一个周期的上升沿时间计数,frq=(80000000/80)/得到的时间值。测量占空比的原理:高电平的时间比上一个周期的时间。在定时器的另外一个通道,配置成间接模式Input Capture indirect mode,直接模式去测上升沿,间接模式去测下降沿。Polarity Selection一个是Rinsing Edge,另一个是falling Edge。打开中断使能。
剩下一个定时器TIM3同理。
setup初始化的地方把定时器中断打开。
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_2);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_2);
my_main.h
//
my_main.c
double val1a=0,val2a=0;
uint val1b=0,val2b=0;
uint frq1=0,frq2=0;
float duty1=0,duty2=0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM2)
{
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
{
val1a=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
val1b=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
__HAL_TIM_SetCounter(htim,0);
frq1=(80000000/80)/val1a;
duty1=(val1b/val1a)*100;
HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_1);
HAL_TIM_IC_Start(&htim2,TIM_CHANNEL_2);
}
}
if(htim->Instance==TIM3)
{
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
{
val2a=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
val2b=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
__HAL_TIM_SetCounter(htim,0);
frq2=(80000000/80)/val2a;
duty2=(val2b/val2a)*100;
HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_1);
HAL_TIM_IC_Start(&htim3,TIM_CHANNEL_2);
}
}
}
`HAL_TIM_IC_Start`: 这个函数用于启动定时器的输入捕获模式,但不启用中断。当只需要获取输入捕获的值而不需要中断处理时,可以使用这个函数。
`HAL_TIM_IC_Start_IT`: 这个函数不仅启动了定时器的输入捕获模式,还启用了中断。这样,当输入捕获事件发生时,会触发中断,并调用相应的中断处理函数。这个函数适用于需要在输入捕获事件发生时执行某些特定操作的情况。
如果中断回调函数太长记不住也可以在如下文件的后部分找到
ADC_DMA
单通道
用到接口PB15和PB12选择ADC IN,左边Analog里面找到PB15用到的ADC1,ADC1里面找到用到的IN 11,选择single-ended,ADC2中的IN15同理。采样时间增大对于稳定结果的作用有限,这里采用一个过采样。
.h文件代码
#include "adc.h"
float GETADC_value(ADC_HandleTypeDef *ADCx);
setup校准ADC1
HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);
.c文件代码
float GETADC_value(ADC_HandleTypeDef *ADCx)
{
float prpt;
HAL_ADC_Start(ADCx);
prpt=HAL_ADC_GetValue(ADCx);
return prpt*3.3f/65536.0f;
}
一般要求浮点保留两位小数,在LCD显示时形式为 V:=%.2f V:=后面要跟数字,数字是浮点型保留两位小数
定义函数时要写函数返回值类型,后面形参括号里面写形参或者void,主函数调用时就只用写函数名加分号。
获取值代码
ADC_value=GETADC_value(&hadcx);//传参数例如&hadc1给上面的ADCx
DMA(建议)
使用PB12和PB14演示,PB15已经用以上单通道演示过。
使能引脚单端触发,添加DMA设置为扫描模式。参数设置中通道数设置为2,使能扫描模式、连续请求模式、DMA连续请求。使能定时请求,通道采样时间尽可能大,否则占用过多CPU程序可能卡死。
开头设置缓冲区
uint16_t adc_buf[2];
setup中校准和开启DMA
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);
HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_buf,2);
数据存在了缓冲区adc_buf[0]和adc_buf[1],可以随时使用,例如显示在屏幕上 ,但是要经过一下处理,例如保留三位小数adc_buf[0]*3.3f/4096.0f,adc_buf[1]*3.3f/4096.0f
char text[30];
sprintf(text,"ADC1:%.3f,2:%.3f",adc_buf[0]*3.3f/4096.0f,adc_buf[1]*3.3f/4096.0f);
LCD_DisplayStringLine(Line0,(uint8_t *)text);
USART
空闲中断回调函数(简单)
接受时所使用的空闲中断回调函数是只有在V1.4.0及其之后的版本固件包才有,没有此函数需要用中断回调函数,会麻烦的多,但是可以实现,见后文。
关于格式化输入输出:
例:sscanf(rxdata,"%c",&uart_flag[3]);中的%c是char类型,也可以转化为string类型,需要写为%xs,x为数字,是要转化的长度,例如
sscanf(rxdata,"%4s:%4s:%12s",car_type,car_data,car_time);
在这个sscanf语句中,冒号在格式字符串中的作用是用于匹配输入字符串中的冒号字符。这表示在rxdata中,字符串应该以冒号分隔,并且sscanf将会按照这种格式解析输入字符串。
具体来说:
“%4s” 表示读取最多4个非空格字符的字符串,这里用于匹配 car_type。“:” 表示在输入字符串中匹配冒号字符。
“%4s” 再次表示读取最多4个非空格字符的字符串,用于匹配 car_data。“:” 再次表示匹配冒号。
“%12s” 表示读取最多12个非空格字符的字符串,用于匹配 car_time。
这样的格式说明符确保了输入字符串的特定格式,其中每个字符串之间由冒号分隔。如果rxdata不符合这种格式,sscanf可能无法正确解析字符串,或者解析结果可能不符合预期。
C语言中,& 操作符用于获取变量的地址。在 sscanf 函数中,如果你想将读取到的值存储到变量中,需要传递该变量的地址。如果不使用 &,则传递的是变量的值,而不是地址。
对于数组来说,数组名本身就是数组的地址。所以,当你传递数组名时,不需要使用 &。但如果你传递数组的某个元素(如字符或者整型数),那么需要使用 & 来获取该元素的地址。如下:
char usart_flag[5];
// 不使用&,传递的是数组的地址
sscanf(rxdata, "%c", usart_flag);
// 使用&,传递的是数组的第一个元素的地址
sscanf(rxdata, "%c", &usart_flag[0]);
Cubemx配置,在通信位置connectivity选择uart1,mode改成第二项异步模式Asynchronous。没有配置过LCD的话会默认使用PC4和PC5,需要手动改成PA10和PA9。Baud rate波特率一般使用9600,nvic setting中断打开。
空闲中断回调函数位置,建议复制粘贴
setup
HAL_UARTEx_ReceiveToIdle_IT(&huart1,(uint8_t *)uart_rx,50);
开启中断只能使用一次,所以接收函数最后还要再开启一次
.h
#include "usart.h"
#include "string.h"
void uart_tx_proce(void);
.c(以接收到四位字符串为例)
char uart_tx[50];
char uart_rx[50];
char text1 [4]="0000";
char text[30];
void uart_tx_proce(void)
{
sprintf(uart_tx,"tx");
HAL_UART_Transmit(&huart1,(uint8_t *)uart_tx,strlen(uart_tx),50);
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
sscanf(uart_rx,"%3s",text1);
sprintf(text,"text1:%s",text1);
LCD_DisplayStringLine(Line1,(uint8_t *)text);
HAL_UARTEx_ReceiveToIdle_IT(&huart1,(uint8_t *)uart_rx,50);
}
另一种中断回调函数方法(应该用不到)
.c文件中利用中断回调函数进行接收,然后进行处理(示例)及发送写法
char rxdata[30];
uint8_t rxdat;
uchar rx_pointer;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//接收中断
{
rxdata[rx_pointer++]=rxdat;
HAL_UART_Receive(&huart1,&rxdat,1,10);
}
//char uart_data[3];
void uart_rx_proce()//数据接收处理程序
{
if(rx_pointer>0)//说明有数据被接收,进行处理
{ /*
if(rx_pointer=1) 恰好接收一位数据
sscanf(rxdata,"%c",&uart_data[3]); 格式化处理接收到的数据
else
{
char temp[20]; 发送示例
sprintf(temp,"Error"); 发送示例
HAL_UART_Transmit(&huart1,(uint8_t *)temp,strlen(temp),50); 发送示例
}*/
rx_pointer=0;memset(rxdata,0,30);//清零
}
}
在 HAL_UART_RxCpltCallback 回调函数中,HAL_UART_Receive_IT 函数首先被调用,它开始等待接收一个字节的数据。然后,rxdata[rx_pointer++]=rxdat; 语句被执行,将接收到的数据存储在 rxdata 数组中,并且 rx_pointer 递增。因此,rx_pointer 表示已经接收到的数据数量。虽然 rxdata[rx_pointer++]=rxdat; 在 HAL_UART_Receive_IT 函数之后,但由于这是在回调函数中,它们实际上是异步执行的。即,当有数据可用时,回调函数被触发,首先会启动下一次接收,然后处理已接收的数据。所以,rxdata[rx_pointer++]=rxdat; 的执行确实发生在HAL_UART_Receive_IT(&huart1,&rxdat,1); 之后。
在C语言中,`uint8_t`是一个无符号的8位整数类型,因此可以用来接收字符。字符在C语言中实际上是以ASCII码的形式存储的,而ASCII码的取值范围是0到255,恰好与`uint8_t`类型的取值范围一致。因此,可以使用`uint8_t`类型来接收字符。
main.c中进行初始化
extern uint8_t rxdat;
HAL_UART_Receive_IT(&huart1,&rxdat,1);
SYSTICK
只需要知道SysTick_Handler(void)是一毫秒进入一次的函数,至于上面编写何种功能的函数,然后放在SysTick_Handler(void)里面从而实现何种功能就根据实际而来,这里的是0.1s反转flag的函数。
//stm32Gxx_it.c里面
//------------------------------------------
int flag=0;
int count=0;
void sys_tim(int time_set)//time_set放在一毫秒进入一次的中断SysTick_Handler(void)里面,所以单位是毫秒
{
count++;
if(count==time_set)
{
flag=!flag;
count=0;
}
}
//------------------------------------------------
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
sys_tim(100);//可以改变为其他时间
/* USER CODE END SysTick_IRQn 1 */
}
//控制LED可在其他地方引用flag
还可以用另一种由系统设置好的变量来操作,原理一样,每1ms执行一次HAL_InTick函数
HAL_InTick的具体作用是自增1
在程序初始化位置定义计时节点 ,500ms=uwTick是定时开始节点
uint32_t 500ms;
500ms=uwTick;
在主程序中书写需要定时的操作,类似地,还可以同时存在其他时长的定时程序,但是此方法精确度不太高。 而且只适合于从上电开始就一直执行的定时程序,如果需要触发的定时,建议设置标志位,然后通过标志位进入SysTick_Handler(void)进行计数,从而特定事件触发,然后执行完毕后清除触发标志位。
if(uwTick-500ms>500)
{
//此处写需要具体执行的操作
500ms=uwTick;
}
RTC实时时钟
以秒为单位进行计时,相当于钟表,配置时最下方year会自动补齐20xx,所以20不用写
.h
#include "rtc.h"
如果只读取时间,也需要把日期读一下,只有日期读完后,数据才会从影子寄存器读取。
.c,sprintf格式化部分多打几个空格清屏防止残留。
void RTC_proce(void)
{
RTC_TimeTypeDef time;
RTC_DateTypeDef date;
HAL_RTC_GetTime(&hrtc,&time,RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc,&date,RTC_FORMAT_BIN);
char text[30];
sprintf(text,"time:%d:%d:%d ",time.Hours,time.Minutes,time.Seconds);
LCD_DisplayStringLine(Line0,(uint8_t *)text);
}
IIC-EEPROM
eeprom有寿命,尽量不要频繁写入,以下时序图赛场会芯片手册会提供
复制到bsp文件夹并加入工程,内部是一些时钟线的配置和应答函数等,所以不需要cubemx配置
根据读写时序图编写写入和读取函数
my_main.h包含i2c_hal.h
#include "i2c_hal.h"
void eeprom_write(uint8_t addr,uint8_t data);
uint8_t eeprom_read(uint8_t addr);
void eeprom_write16(uint8_t addr,uint16_t data);
uint16_t eeprom_read16(uint8_t addr);
void eeprom_write_f(uint8_t addr,float data);
float eeprom_read_f(uint8_t addr);
setup中初始化
I2CInit();
.c
void eeprom_write(uint8_t addr,uint8_t data)
{
I2CStart();//开启总线
I2CSendByte(0xA0);//联系芯片 1010 0000
I2CWaitAck();//等待应答
I2CSendByte(addr);//写入地址
I2CWaitAck();//等待应答
I2CSendByte(data);//传入数据
I2CWaitAck();//等待应答
I2CStop();//停止总线
}
uint8_t eeprom_read(uint8_t addr)
{
uint8_t rec;
I2CStart();//开启总线
I2CSendByte(0xA0);//联系芯片 1010 0000
I2CWaitAck();//等待应答
I2CSendByte(addr);//写入地址
I2CWaitAck();//等待应答
I2CStop();//停止总线
//第一轮通讯先写入
I2CStart();//开启总线
I2CSendByte(0xA1);//联系芯片 1010 0001 读写控制位改为读(1)
I2CWaitAck();//等待应答
rec=I2CReceiveByte();//接到返回值
I2CSendNotAck();//发送非应答信号
return rec;
}
读写地址一致,比如写在第八个位置
eeprom_read(8);
写入值后按下复位键,读取的和写入的一样就是实现。每一个位置只能存储8位数据,存储16位需要拆分。
void eeprom_write16(uint8_t addr,uint16_t data)
{
uint8_t data_h=data>>8;
uint8_t data_l=data<<8;
eeprom_write(addr,data_h);
HAL_Delay(10);
eeprom_write(addr+1,data_l);
}
uint16_t eeprom_read16(uint8_t addr)
{
uint8_t data_h=eeprom_read(addr);
uint8_t data_l=eeprom_read(addr+1);
return (data_h<<8)+data_l;
}
如果要存储小数就把小数乘一个10的倍数,读出来时再除以一下
void eeprom_write_f(uint8_t addr,float data)
{
uint16_t data_f=data*100;
uint8_t data_h=data_f>>8;
uint8_t data_l=data_f<<8;
eeprom_write(addr,data_h);
HAL_Delay(10);
eeprom_write(addr+1,data_l);
}
float eeprom_read_f(uint8_t addr)
{
uint8_t data_h=eeprom_read(addr);
uint8_t data_l=eeprom_read(addr+1);
return ((data_h<<8)+data_l)/100.0f;
}
IIC-数字电位器
写入数据0-127对应0-100k的阻值,数据写入时转化为16进制传参
.h
void mcp_write(uint8_t data);
.c代码
void mcp_write(uint8_t data)//0-127对应0-100k的阻值
{
I2CStart();//开启总线
I2CSendByte(0x5e);//联系芯片 0101 1110
I2CWaitAck();//等待应答
I2CSendByte(data);//刺蛾如数据
I2CWaitAck();//等待应答
I2CStop();//停止总线
}
传参写十进制。若想获取PB14的分压值,可以使用上面的DMA多通道采样。