目录
初始配置
打开你的stm32CubeMX,选择File下的Nex Project
选择板子,创建工程
配置RCC(深色部分)
配置SYS
时钟配置(1,2步没有先后,2步时输入后需要确定,推荐使用170,3步是2步按下enter后才出现的)
工程管理中工程配置(第1步建议按照省赛题目要求取名,提交以准考证号命名的hex文件,虽然后面可以改hex文件名字)
工程管理中的代码生成以及生成项目(4步是在3步后出来的,一次生成的时候建议直接打开项目,后面可以直接去keil中打开,然后编译一下,)
keil中project配置
keil中配置2
最后编译一下(第一次工程的时候,可以选1,后面通过stm32cubeMX添加时,选2快一点,记得每次stm32cubeMX改进后需要重新编译一下)
led模块
介绍:由图可知,led1-led8由PC8-PC15的引脚控制,同时对应的引脚置位低电位,对应的灯才会亮,因为PC8-PC15与lcd的引脚共用了,所以当PD2为高电位时,PC8-PC15作用与led,同理,为低电位时,作用于lcd。因为比赛中lcd需要一直显示,同时肯能需要快速变换数值,而led作用赋值后,对应led会保留赋值状态,所以PD2只有再要改变led是才处于高电位,同时改变完后需要重新置位低电位。
stm32cubeMX中配置,只需要对应引脚输入成GPIO_output
led代码
uint8_t led_time,led_flag;//全局变量默认值为0
void led_change(uint8_t led_val){
HAL_GPIO_WritePin(GPIOC,~(led_val)<<8,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC,led_val<<8,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
/*
举个例子,打开led1,关闭led2,100ms反转一次led3
*/
uint8_t led_num=0x00;
void led_prc(){
led_num|=0x01;
led_num&=0xfd;
if(led_flag){
led_num^=0x04;
led_flag=0;
}
led_change(led_num);
}
//这里拿一个已经弄好的定时器,10ms进入一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim->Instance==TIM3){
if(led_flag==0)
led_time=(led_time+1)%10;
if(led_time==0&&led_flag==0)
led_flag=1;
}
}
//led的初始化,stm32cubeMx配置好后不需要配置了
int main(){
led_change(0x00);记得全部关闭灯,初始式是全亮的
while(1){
led_prc();
}
}
lcd模块
介绍:官方提供了代码,需要知道其的具体位置,把要用的三个文件放入工程中去。lcd中一般设置为白字黑底,还有就是注意位置和字符串显示到lcd中去
stm32cubeMX中lcd配置(对应引脚设置为GPIO_Output就行)
lcd代码
#include "lcd.h"
#include "stdio.h"
#include "string.h"
char str[20];
uint8_t lcd_time;
void lcd_prc(){
if(lcd_time<30)//有pwm时推荐100ms,没有时推荐300ms
return ;
else
lcd_time=0;
sprint(str," M:%d%% ",20);//及得多加空格,防止数据长度变换导致数据清理不干净,添加%号
LCD_DisplayStringLine(Line3,(uint8_t *)str);
sprint(str," N:%.1f ",3.21);//保留一位小数
LCD_DisplayStringLine(Line4,(uint8_t *)str);
}
//这里拿一个已经弄好的定时器,10ms进入一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim->Instance==TIM3){
lcd_time++;
}
}
int main(){
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
while(1){
lcd_prc();
}
}
定时器模块
介绍:最常用的为用来,进行时间上的把控,所以一般使用打开中断的定时器。时间=1/频率,频率=总频率(80*1e6)/((预分频计数器值+1)*(重置载值+1)),所以时间=((预分频计数器值+1)*(重置载值+1))/(80*1e6),所以设置一个为10ms的定时器,频率应为100Hz,预分频计数器值为Prescaler,重置载值为Counter Period
stm32cubeMX配置(第5步去NVIC打开中断)
定时器代码
//10ms进入一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim->Instance==TIM3){//用定时器几,就写TIM几
}
}
ing main(){
HAL_TIM_Base_Start_IT(&htim3);//开启定时器中断
while(1){
}
}
pwm输出模块
介绍:pwm输出需要通过定时器进行输出,题目一般要求输出一定的频率和占空比,或则更改频率与占空比。由定时器模块我没可以知道频率的具体计算,pwm输出不需要打开定时器中断,占空比=脉冲数(pluse)/重置载值(Counter Period ),更改频率推荐更改重装载值,因为它提供了快速响应和高精度调整的能力,同时对其他定时器功能的影响较小。
stm32cubeMX配置(1kHz,占空比为20%)
pwm输出模块代码
int main(){
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
while(1){
/*
更改频率为2KHz,占空比为40%,
(当然更改频率肯定不能放入while中,要更改的话更改一次就行)
*/
__HAL_TIM_SetAutoreload(&htim2,500-1);//更改重装载值(Counter Period)
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,500*40/100);//设置脉冲值
/*
__HAL_TIM_SET_PRESCALER(&htim2, 40-1);//更改预分频寄存器的数值 Prescaler
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,1000*40/100);//设置脉冲值
//(建议别这样设置)
*/
}
}
频率捕获模块
介绍:频率捕获通过定时器通道进行捕获,这个需要开启定时器中断进行捕获,同时预分频计数器的值时固定的,为你设置的主频-1,同时要注意重装载值所能捕获的最大数值,我设置的这个定时器为65535
stm32cubeMX
频率捕获代码
uint32_t frq;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){//中断读取
uint32_t val;
if(htim->Instance==TIM17){
val=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1)+1;
__HAL_TIM_SET_COUNTER(htim,0);
frq=1000000/val;//注意这个当读取3000hz的频率是,会出现问题,这是正常的
HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);
}
}
int main(){
HAL_TIM_IC_Start_IT(&htim17,TIM_CHANNEL_1);
while(1){
}
}
板子上的相关模块介绍:可以看到R40与R39是板子4个旋钮中的两个,它们通过旋钮调节可以产生不同频率的输出,可以看到R40产生的通过PA15接收,R39通过PB4接收,当然,你可以把J9拔掉,将2接到我们之前设置的PA7,然后就可以通过lcd显示出具体数只,还能通过旋钮进行调节
按键模块
介绍:按键模块有四个,但有按键按下时,对应的引脚变为低电位,当然为了防止按下抖动,需要进行10ms延时后再判定
stm32cubeMX(将对应的引脚设置为GPIO_Input)
按键模块代码:介绍两种,先简单一点的,不用定时是器的,这种也没啥弊端,但处理不了长按键,有人可能说不是直接10ms延时了吗,但这种要有按下才会触发,当然也有可能抖动触发,但10ms也挺短的,当然一直按着就会存在10ms一个while(eeeee)
uint8_t key_read(){
uint8_t key_val=0;
if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)&HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)&HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)&HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0))==0){
HAL_Delay(10);//延时10ms
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==0)
key_val=1;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)==0)
key_val=2;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)==0)
key_val=3;
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0)
key_val=4;
}
return key_val;
}
void key_prc(){
static uint8_t key_val=0;
uint8_t key_temp=key_read();//按下的时候,key_temp!=0,同时二值不相等
if(key_val!=key_temp){//抬起虽然也不相等,当key_temp为0
key_val=key_temp;
if(key_val==1){
}
if(key_val==2){
}
if(key_val==3){
}
if(key_val==4){
}
}
}
int main(){
while(1){
key_prc();
}
}
再介绍一个定时器中断的方法,每10ms进行一次按键判断,保留之前的按键转态,(好像一想其实还有中断按键判断,还不需要放到while中去,虽然也不好进行长按键判断,但肯定比第一个方法好多了,一直按着也不会再次触发中断,只会在按下的时候触发一次中断,对应的引脚也有,GPIO_EXTI)
uint8_t key_old,key_flag,key_time,key_long_flag,key_long_time_flag;
uint16_t key_long_time;
//配置好的1ms定时器
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim->Instance==TIM3){
if(key_flag==0)
key_time=(key_time+1)%10;
if(key_time==0&&key_flag==0)
key_flag=1;
if(key_long_flag)
key_long_time=(key_long_time+1)%2000;
if(key_long_time==0&&key_long_flag){
key_long_time_flag=1;
}
}
}
}
uint8_t key_read(){
uint8_t key_val=0;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==0)
key_val=1;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)==0)
key_val=2;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)==0)
key_val=3;
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0)
key_val=4;
return key_val;
}
void key_prc(){
uint8_t key_up,key_down,key_now;
key_now=key_read();
key_down=(key_now)&(key_now^key_old);//(举例一下key1按下)按下瞬间,key_now为1,而key_old为0,则所得值为1,抬起该值为0
key_up=(~key_now)&(key_now^key_old);//抬起数据,key_now为0,而key_old为1,则所得值为1,按下该值为0
key_old=key_now;
if(key_down==1){//按下
}
//这里展示一下长按键,2s为例
if(key_down==2){
key_long_flag=1;
key_long_time=0,key_long_time_flag=0;
}
if(key_up==2){
key_long_flag=0;
}
}
int main(){
while(1){
if(key_flag){//10ms进行判断
key_flag=0;
key_prc();
if(key_long_time_flag){//按下时间超过2ms,需要进行的操作可以写到括号里面
key_long_time_flag=0;
key_long_flag=0;
}
}
}
}
串口模块
介绍:PA9和PA10与串口1连接,可以与电脑的串口段进行通信
stm32cubeMX配置(第4步配置注意看题目要求)
usaet1代码
#include "stdio.h"
#include "string.h"
uint8_t rx_len=0;
char rx_str[30];//这个长度根据题目要求进行更改
uint8_t rx_ch;
char str;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){//接受数据
if(huart->Instance==USART1){
rx_str[rx_len]=rx_ch;
rx_len=(rx_len+1)%30;
UART_Start_Receive_IT(huart,&rx_ch,1);
}
}
void Uart1_prc(){
uint8_t rx_temp_len;
rx_temp_len=rx_len;
if(rx_temp_len!=0){
HAL_Delay(1);//这里最好的是搞个定时器,弄个10ms的,不影响总进程,10ms后再来比较,当然这太麻烦了
if(rx_temp_len==rx_len){
if(strcmp(rx_str,"hello")==0){
sprintf(str,"world");
HAL_UART_Transmit(&huart1,(uint8_t *)str,strlen(str),50);//发送数据到电脑端
}
else{
sprintf(str,"error");
HAL_UART_Transmit(&huart1,(uint8_t *)str,strlen(str),50);
}
rx_len=0;
memset(rx_str,0,strlen(rx_str));
}
}
}
int main(){
UART_Start_Receive_IT(&huart1,&rx_ch,1);
while(1){
Uart1_prc();
}
}
adc模块
介绍:对已与板子上的R37与R38旋钮,可以通过旋钮进行电压的调节
stm32cubeMX(这个选择Adc_INX就行)
附加(可以把ADC2_IN15的这个加号也点了,ADC1_IN11也有)
adc读取代码
float adc_value1,adc_value2;
float Adc_value(ADC_HandleTypeDef *hadc){
uint16_t value=0;
HAL_ADC_Start(hadc);
value=HAL_ADC_GetValue(hadc);
return value*3.3/4095;
}
int main(){
while(1){
adc_value1=Adc_value(&hadc2);
adc_value2=Adc_value(&hadc1);
}
}
adc多通道
介绍:我这里提供两种方式,一直是直接的单通道不连续的扫描模式,还有一直是dma的协助,dma感觉数值准确一点,但我不知道为啥,dma读取的第一组数据的第一个数有问题,所以我直接拿第二组的第一个数来当第一个的数值,我就选adc1了,一个对应板子的mcp4017,一个对应板子的模拟输出R38
stm32cubeMX配置(第一种方式与第二重方式都要进行的步骤,注意1步是我PB14要读取mcp4017进行的配置,你选择其他通道可以不需要配置)
stm32cubeMX(第一种方式剩余配置,第4步应是不需要要配置的,当你第一步完成后,他会自动变成enable)
代码:
uint16_t adc_val[2];
uint16_t Get_adc(ADC_HandleTypeDef * hadc){
HAL_ADC_Start(hadc);
HAL_ADC_PollForConversion(hadc,100);//开始转换
if(HAL_IS_BIT_SET(HAL_ADC_GetState(hadc),HAL_ADC_STATE_REG_EOC)){//是否转换成功
return HAL_ADC_GetValue(hadc);
}
else
return 0;
}
int main(){
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);//校准过程的函数
while(1){
uint8_t i;
for(i=0;i<2;i++){
adc_value[i]=Get_adc(&hadc1);
}
}
}
stm32buceMx配置(方法2,第4步在第1步配置后就会自动变成enable)
(dma启动步骤)
代码:
uint16_t adc_val[4]
int main(){
while(1){
/*
adc_val[0]与adc_val[2]存储的是同一个值adc5通道的值
adc_val[1]与adc_val[3]存储的是同一个值adc11通道的值
当然4也可以改成6,读取3组数据处理,我只前说过adc_val[0]第一个值数据值不对,所以我直接选adc_val[2]当做adc5通道的值
*/
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)adc_val,4);
}
}
dac模块
介绍:adc都说了,这个顺带讲一下,这个省赛也没考过,这个dac就是可以通过个引脚进行电压输出,这个好的一点就是你一个dac看两个通道不需要其他配置,直接引脚初始话后再在dac中开启就行了,同时也不需要添加GPIO_Analog
stm32cubeMX配置(我这里直接配两个都一样的,配一个的话,第一步少设一个,第三步对应的取消)
代码:
void dac_set(DAC_HandleTypeDef * hadc,uint32_t Channel,float val)
{
HAL_DAC_SetValue(hadc,Channel,DAC_ALIGN_12B_R,val);//0->0V;4095->3.3V
HAL_DAC_Start(hadc,Channel);
}
int main(){
while(1){
dac_set(&hdac1,DAC_CHANNEL_1,(uint16_t)(1.1f/3.3f*4095));
dac_set(&hdac1,DAC_CHANNEL_2,(uint16_t)(2.2f/3.3f*4095));
}
}
eeprom模块
介绍:M24c02就是eeprom,总共256个字节可以用来存储地址,同时每也有8个地址,也就是这个有32页,MCP4017T一个可编程电阻
配置:这个官方也提供了iic的代码(把i2c_hal.c与.h引用到工程中去)
stm32cubeMX配置(设置位GPIO_Output就行,应该是要搞个输入的,应该是提供的代码中会变换吧)
eeprom代码:这里提供两种读和写,一种是单独读取一个地址和写入一个地址,还有一个种是连续的读和写,为啥要弄连续的呢,因为当你连续使用单个写入时,需要中间延时10ms(我建议),否则连续写入会出错,但是你是单个的连续读是不需要延时的(读取过程可以立即开始,并且数据可以在发送读取命令后立即从EEPROM获得),所以呢,连续读其实也没啥用,对了,还需要注意连续读和写的限制,其只能在同一页内进行,也就是最多连续8个字节(一个字节8位),顺便介绍一下应答和非应答
应答(ACK): 当从设备成功接收到一个字节数据后,它会回应一个ACK信号,表示数据已被成功接收并且设备准备好接收下一个字节。
非应答(NACK): 相反,NACK信号用于指示不接受数据或通信结束。发送NACK可以是因为接收到的数据有误,或者在读操作中,主设备通过发送NACK来通知从设备最后一个字节已被接收,接下来将结束通信。
总结:eeprom可以进行多字节存储,它一页有八个字节,所以最多可以进行八个字节的读写,读时需要注意继续读需要发送应答信号,不继续读时需要发送非应答信号。同时可以进行不同数据的读写,double一个数据8个字节,或者进行结构体或者数组的读写
#include "i2c_hal.h"
uint8_t arr[6];
void Read_eeprom(uint8_t addr,uint8_t *data,uint8_t num){//连续读
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
while(num--){
*data++=I2CReceiveByte();
if(num)
I2CSendAck();
else
I2CSendNotAck();
}
I2CStop();
}
uint8_t Read_e2prom(uint8_t addr){//单独读取一个
uint8_t data;
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
data=I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return data;
}
void Write_e2prom(uint8_t addr,uint8_t data){//单独写
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CSendByte(data);
I2CWaitAck();
I2CStop();
}
void Write_eeprom(uint8_t addr,uint8_t *data,uint8_t num){//连续写
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
while(num--){
I2CSendByte(*data++);
I2CWaitAck();
}
I2CStop();
}
int main(){
/*
有的题目喜欢这样出,我第一次需要写入一个数据,但第二次的时候我需要取都这个数据,这个只要去读一个不需要用的地址,然后随便确定一个数,当然最好选两个地址,举例地址0第一次写入10,后面需要读取
*/
if(Read_e2prom(0x06)==56&&Read_e2prom(0x07)==76){
arr[0]=56,arr[1]=76;
Write_eeprom(0x06,arr,2);
arr[0]=10;
Write_eeprom(0x00,arr,1);
}
else{
arr[0]=Read_e2prom(0x00);
}
//当然eeprom肯定不能翻入while中,我这就演示一下
while(1){
arr[0]=10,arr[1]=10,arr[2]=10,arr[3]=10;
arr[4]=65,arr[5]=56;
Write_eeprom(0x00,arr,sizeof arr);//sizeof是将总字节数除以8位,所以如果是uin16_t娜美就是双倍了
HAL_Delay(10);//写函数直接需要10ms延时
Write_e2prom(0x06,12);
Read_eeprom(0x01,arr,sizeof arr);//这其实地址为0x01,则读取总字节数不能超过7
arr[5]=Read_e2prom(0x00);
}
}
MCP4017模块
介绍:这个换板子后的省赛好像没考过,但手册图上有,其的stm32cubeMx配置和eeprom配置一样的,配置PB6和PB7j就行了,若果想要板子读取PB14的值,就可以将PB14按照adc中的配置,这里我就没有配置了,我在adc多通道中会把配置说一下,然后PB14可以进行一个adc的电压读取,注意看R17电阻为10K,而MCP4017可以看出一个可调的滑动变阻器,当然,电阻设置肯定就是靠iic中的数值写入,同时Mcp4017可调的电阻值为0K-100K,而PB14读取的是Mcp4017所占的电阻值,所以PB14的电压值为Rw/(Rw+10)*3.3,所以范围为0v-3.0v,同时Mcp4017中数值写入是8位的,最高位忽略,即可设置的数值范围为0-127,对应电阻0K-100K,所以你设置的值(val)与电阻Rw的关系为(val&0xef)/127=Rw/100,其与adc输出电压的关系就靠你们自己推了,对了adc读取的是一个0-4095的数值,需要自己转换成电压,这个连续读和写不需要延时
mcp4017代码(iic配置和eeprom配置一样的):
void mcp4017_write(uint8_t value){
I2CStart();
I2CSendByte(0x5e);
I2CWaitAck();
I2CSendByte(value);
I2CWaitAck();
I2CStop();
}
uint8_t mcp4017_Read(){//读取的是你写入的值
uint8_t value;
I2CStart();
I2CSendByte(0x5f);
I2CWaitAck();
value=I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return value;
}
int main(){
uint8_t val;
while(1){
mcp4017_write(127);//adc所都的值应是3.0/3.3*4095
val=mcp4017_Read();//读取的值为127
}
}
一些细节及总结
函数调用放到main之外
输入频率设置为24,最外面那个
注意设置优先级(这里得搞个图)
float和double的数值等于某个值,最好是进行大小比较,如fabs(a-1.0<1e-6或1e-9)(0.000001),同时需要取绝对值,及得加头文件#include "math.h"
循环中i没赋初始值,+1没效果
去.h列表中找不到相应的.h,可能是没有编译adc的读取需要先乘在除单精度运算隐式转换成双精度运算了,在浮点数字后面加上f,编译警告就会消失。
V = (f*2*R*3.14f)/(100*K);
keil中的换行是/r/n
讲下我总结的串口数据读取
sscanf(rx_data, "%9[^:]:%9[^:]:%2d%2d%2d%2d%2d%2d", ...);
这样可以确保即使输入字符串过长,也不会导致缓冲区溢出。在 %9[^:] 中,9 表示最多读取 9 个字符(第 10 个位置留给字符串结束符 '\0')。