drivers_day09

本文深入探讨Linux内核定时器的工作原理,包括硬件定时器的特点、HZ与jiffies的关系,以及如何通过HZ参数配置定时器中断频率。同时,文章详细介绍了Linux内核定时器的实现机制,如使用structtimer_list描述定时器,以及如何避免竞态问题,通过中断屏蔽、原子操作、自旋锁和信号量等手段确保多线程环境下的安全访问。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

回顾:

1.硬件定时器的特点

能够通过编程指定它的工作输出频率,周期性给CPU产生一个时钟中断信号;

linux内核也有对应的时钟中断的处理函数,这个函数被内核周期性的调用;

时钟中断的处理函数:

1.更新jiffies/jiffies_64

2.更新实际时间

3.检查当前进程的时间片

4.检查是否有到期的软件定时器(如果有,执行软件定时器处理函数,然后删除软件定时器)

...

2.HZ,tick,jiffies

HZ:给硬件定时器使用,ARM,HZ=100表明一秒钟产生100次时钟中断

jiffies:内核全局变量,记录自开机以来,发生了多少次时钟中断,每发生一次时钟中断,jiffies+1;

linux内核一般使用jiffies来表示时间

 

3.linux内核定时器

struct timer_list:用于描述定时器

定时器的实现是基于软中断,所以定时器的处理函数不能做休眠动作!

 

4.linux内核的延时函数

纳秒级延时:voidndelay(unsigned long nsecs);

微秒级延时:voidudelay(unsigned long usecs);

毫秒级延时:voidmdelay(unsigned long msecs);

ndelay/udelay/mdelay

这三种延时都是忙等待!

以下为睡眠延时:

void msleep(unsigned int millisecs);

unsigned long msleep_interruptible(unsignedint millisecs);

void ssleep(unsigned  int seconds);

/msleep/ssleep

jiffies/内核定时器/schedule_timeout

 

5.linux内核并发和竞态

5.1概念:

并发

竞态

互斥访问

临界区

共享资源

 

5.2竞态产生的情形:

1.多核

2.进程和进程的抢占

3.中断和进程

4.中断和中断

明确:进程的调度,抢占都是基于软中断来实现!

 

5.3linux内核解决竞态的方法:

1.中断屏蔽

2.原子操作

3.自旋锁

4.信号量

总结:让一个执行单元在访问临界区的执行路径具有原子性(不可打断)!

************************************************************************************

Day09

中断屏蔽:

能够解决如下竞态问题:

1.进程和进程之间的抢占

2.中断和进程

3.中断和中断

 

使用方法:

1.访问临界区之前屏蔽中断

  unsigned long flags;

  local_irq_disable();

   或者

  local_irq_save(flags);

2.访问临界区

3.访问完临界区以后,一定要使能中断

  local_irq_enable();

   或者

  local_irq_restore(flags);

4.注意:屏蔽中断和使能中断必须成对使用!

 

关键注意点:在驱动编程时,如果考虑到竞态问题,并且采用中断屏蔽来实现互斥访问,一定要明确屏蔽中断之后,在执行临界区的代码时,速度一定要快,更不能做休眠动作!

因为中断对于linux系统的运行至关重要,硬件中断,进程的调度,抢占,定时器等都是依赖于中断来实现!

 

**********************************************************

原子操作:

笔试题:请实现将一个数的某个bit1或者清0

第一个同学:

int data = 0x12345;

data |= (1 << 5);

data &= ~(1 << 5);

 

第二个同学:

void set_bit(int nbit, int *data)

{

       ...

}

 

第三位同学:

void set_bit(int nbit, void *data)

{

if(n<8)

 *(char*)data |=(1<<n);

  elseif(n<16)

  *(short*)data|=(1<<n);

  elseif(n<32)

  *(int*)data  |=(1<<n);

}

 

第四位同学:

#define SET_BIT(nr, data) ...

#defineset_bit(n,data) (data |=(1<<n))

还是利用宏函数的方法好!

基于linux系统的参考答案(GNU C):

inline void set_bit(int nr, void *data)

{

       ...

}

 

linux内核原子操作:

原子操作能够解决所有竞态的问题;

原子操作分为:

位原子操作:

如果以后驱动中对共享资源进行位操作,并且为了避免竞态问题,一定要使用内核提供的位原子操作的方法,保证位操作的过程是原子的,不能自己去实现位操作,例如:

static int data; //全局变量,共享资源

 

//临界区

data |= (1 << 5); //这个代码不是原子的,有可能被别的任务打断!

如果不考虑多核引起的竞态,还有一种通过中断屏蔽来解决以上代码的竞态问题

unsigned long flags;

local_irq_save(flags);//禁止中断

data |= (1 << 5); //访问临界区

local_irq_restore(flags);//使能中断

注意以上代码无法解决多核引起的竞态!

 

内核提供的位原子操作的方法:

set_bit/clear_bit/change_bit/test_bit/组合函数

对于data进行位操作的正确做法:

static int data;

set_bit(5, &data); //这个代码是原子的,不能被别的任务打断

 

注意:以上函数在多核情况下,会使用两条ARM的原子指令:

ldrex,strex,这个两条保证在CPU那一级别能够避免竞态,以上函数都是采用C的内嵌汇编来实现,如果用C语言来实现,编译器肯定是用ldr,str,但这个两条指令不能避免竞态!

 

案例:利用位原子操作将0x5555->0xaaaa,不允许使用change_bit函数!

 

整型原子操作:

如果以后驱动程序中,涉及的共享资源是整型数,就是原型要定义为char,short,int,long型的数据,并且它们是共享资源,为了避免竞态,可以考虑使用内核提供的整型原子操作机制来避免竞态问题。

说白了就是将原先的char,short,int,long类型换成atomic_t数据类型即可,然后配合内核提供的整型原子操作的函数进行对整型变量进行数学运算!

 

整型原子变量的数据类型:

atomic_t

 

如何使用:

分配整型原子变量

atomic_t v;

进行对整型变量的操作:

atomic_set/atomic_read/atomic_add/atomic_sub/atmoioc_inc/atomic_dec/atomic_inc_and_test...

对整型变量的操作一定要使用以上的函数进行,保证具有原子性。不能使用如下代码:

static int data; //全局变量,共享资源

 

//临界区

data++; //不是原子的!有可能被打断

 

解决的方法:

如果不考虑多核:

unsigned long flags;

local_irq_save(flags);

data++;

local_irq_restore(flags);

如果考虑多核:

atomic_t data;

atomic_inc(&data);

 

注意:以上整型原子操作的函数,如果在多核情况下,它们的实现都是C的内嵌汇编来实现的,都调用了ldrex,strex来避免竞态

 

案例:实现LED灯驱动,要求这个设备只能被一个应用程序打开

分析:

明确:app会调用open打开设备,close关闭设备

驱动:一定要提供对应的底层open,close的实现,注意不能省略底层这两个函数,因为需要在底层的open,close函数中做一些用户需求的代码(设备只能被一个应用程序打开)

方案:

static int open_cnt; //可以采用中断屏蔽

方案:

只有open_cnt1时,才能正确打开设备!打开一个设备之后open_cnt变为0,不能再打开设备了!

static atomic_t open_cnt=1; //利用整型原子操作

static int led_open(struct inode *inode,

                        struct file *file)

{

   if (!atomic_dec_and_test(&open_cnt)) {//atomic_dec_and_test(atomic_t*v)

           //执行减操作,如果结果为0,返回true,否则返回false

       printk("设备已被打开!\n");

       atomic_inc(&open_cnt);

       return -EBUSY;//给用户返回打开设备失败,设备忙

    }

   printk("进程打开设备成功!\n");

   return 0;

}

static int led_close(struct inode *inode,

                        struct file *file)

{

   atomic_inc(&open_cnt);

   printk("进程关闭设备!\n");

   return 0;

}

实验步骤:

1.insmod led_drv.ko

2.cat /proc/devices //查看申请的主设备号

3.cat /sys/class/myleds/myleds/uevent //查看创建设备文件的原材料

4.ls /dev/myled //查看设备文件

5. ./led_test & //启动A进程,让其后台运行,A进程进入休眠

6.ps //查看A进程的PID

7.top //查看A进程的状态和CPU的利用率,内存使用率

8../led_test //启动B进程

9.kill A进程的PID //杀死A进程

 

问题:

~ # rmmod led_drv

rmmod: can't unload 'led_drv': Resourcetemporarily unavailable

原因是驱动程序正在被一个进程占有!

 

******************************************************************************************

自旋锁:等于“自旋” + ”锁“

自旋锁特点:

1.自旋锁一般要附加在共享资源上;类似光有自行车锁,没有自行车是没有意义!

2.自旋锁的“自旋”的意思是想获取自旋锁的执行单元,在没有获取自旋锁的情况下,原地打转,忙等待着获取自旋锁;

3.一旦一个执行单元获取了自旋锁,在执行临界区时,不要进行休眠操作。“不够意思”。

4.自旋锁也是让临界区的访问具有原子性!

 

linux内核如何描述一个自旋锁:

数据类型:spinlock_t

 

如何使用自旋锁来对临界区进行互斥访问:

static int open_cnt; //全局变量,共享资源

1.分配自旋锁

  spinlock_t lock

2.初始化自旋锁

  spin_lock_init(&lock);

3.访问临界区之前获取自旋锁,进行锁定

   spin_lock(&lock); //如果执行单元获取自旋锁,函数立即返回,如果执行单元没有获取锁,执行单元不会返回,而是原地打转!处于忙等待,直到持有自旋锁的执行单元释放自旋锁。

   或者:

   spin_trylock(&lock);//如果执行单元获取自旋锁,函数返回true,如果没有获取自旋锁,返回false,不会原地打转!

 

4.执行临界区的代码

  if(--opencnt != 0) {

       ....

  }

这个过程其他CPU或者本CPU的抢占进程无法来执行临界区,但是还会被中断所打断!如果考虑中断的因素,要使用衍生自旋锁

 

5.释放自旋锁

   spin_unlock(&lock);//获取锁的执行单元释放锁,然后等待获取锁的执行单元停止原地打转而是获取自旋锁,然后开始对临界区的访问。

 

注意:以上自旋锁的操作只能解决多CPU和本CPU的进程抢占引起的竞态,但是无法处理中断引起的竞态,如果考虑到中断,必须采用衍生自旋锁!

 

衍生自旋锁本质上其实就是在普通的自旋锁的基础上进行屏蔽中断和使能中断的动作。

衍生自旋锁的使用:

static int open_cnt; //全局变量,共享资源

1.分配自旋锁

  spinlock_t lock

2.初始化自旋锁

  spin_lock_init(&lock);

3.访问临界区之前获取自旋锁,进行锁定

   spin_lock_irq(&lock);//屏蔽中断,获取自旋锁

   或者

   spin_lock_irqsave(&lock,flags);//屏蔽中断,保存中断状态,获取自旋锁

  

4.访问临界区

   if(--opencnt != 0) {

       ....

  }

5.释放自旋锁

   spin_unlock_irq(&lock);//释放自旋锁,使能中断

   或者

   spin_unlock_irqrestore(&lock,flags);//释放自旋锁,使能中断,保存中断状态

 

注意:衍生自旋锁能够解决所有的竞态问题!

 

自旋锁使用的注意事项:

1.一旦获取自旋锁,临界区的执行速度要快,更不能做休眠动作

 

案例:利用自旋锁,来实现一个设备只能被一个应用程序打开

 

*********************************************************

linux系统进程的状态:三个状态

1.进程的运行状态,linux系统描述运行中的进程通过TASK_RUNNING来表示!

2.进程的准备就绪状态,linux系统描述进程准备就绪用

TASK_READY来表示

3.进程的休眠状态,进程的休眠状态又分

3.1不可中断的休眠状态,linux系统描述用TASK_UNINTERRUPTIBLE来表示,如果进程的休眠状态为不可中断的休眠状态,在休眠期间,如果接收到了信号,不会立即处理信号,但是被唤醒以后,会判断之前是否接收到信号,如果有,那么处理信号!

 

3.2可中断的休眠状态,linux系统描述用TASK_INTERRUPTIBLE,在休眠期间,如果接收到了信号,会被信号唤醒,并且立即处理信号!

 

信号量:

由于自旋锁在访问临界区的时候,要求临界区不能做休眠动作,但是在某些场合,可能需要在临界区做休眠动作,又要考虑竞态问题,此时可以使用信号量来保护临界区

信号量的特点:

1.又叫“睡眠锁”;

2.如果一个执行单元想要获取信号量,如果信号量已经被别的执行单元给持有,那么这个执行单元将进入休眠状态;直到持有信号量的执行单元释放信号量为止。

3.已经获取信号量的执行单元在执行临界区的代码时,也可以进行休眠操作!

4.明确信号量能让进程休眠!

 

linux内核描述信号量的数据类型:

struct semaphore

 

如何使用信号量:

1.分配信号量

   struct semaphore sema

2.初始化信号量为互斥信号量

   sema_init(&sema, 1);

3.在访问临界区之前获取信号量,对临界区进行锁定

   down(&sema); //获取信号量,如果信号量已经被别的任务给持有,那么进程将进入不可中断的休眠状态;

   或者:

   down_interruptible(&sema);//获取信号量,如果信号量已经被别的任务给持有,那么进程将进入可中断的休眠状态;一般在使用的时候一定要对这个函数的返回值进行判断,如果函数返回0,表明进程正常获取信号量,然后访问临界区;如果函数返回非0,表明进程是由于接收到了信号引起的唤醒;

   if(down_interruptible(&sema)) {

      printk("进程被唤醒的原因是接收到了信号");

      return -EINTR;

  }else {

      printk("正常获取信号量引起的唤醒");

      printk("进程可以访问临界区");

  }

  注意:以上两个获取信号量的方法不能用于中断上下文中

 

   或者:

   down_trylock(&sema);//获取信号量,如果没有获取信号量,返回false,如果获取信号量,返回true.所以对返回值也要做判断

   if(down_trylock(&sema)) {

       printk("无法获取信号量");

       return-EBUSY;

 }else {

     printk("获取信号量");

     printk("访问临界区");

}

 

4.访问临界区

5.释放信号量

   up(&sema); //一方面会释放信号量,另一方面还要唤醒之前休眠的进程

 

案例:采用信号量,实现一个设备只能被一个应用程序所打开;

对于这个案例,无需给定一个共享资源,如果A进程打开设备获取信号量,B进程也尝试打开设备,驱动直接让B进程进入休眠状态,直到A进程关闭设备释放信号量为止!

 

实验步骤:

down_interrruptible:

insmod led_drv.ko

./led_test & //启动A进程

./led_test & //启动B进程

ps //查看A,B进程的PID

top //查看A,B进程的状态

kill A进程

kill B进程

./led_test & //启动A进程

./led_test & //启动B进程

ps //查看A,B进程的PID

kill B进程

kill A进程

 

down:

./led_test & //启动A进程

./led_test & //启动B进程

ps //查看A,B进程的PID

top //查看A,B进程的状态

kill A进程

kill B进程

./led_test & //启动A进程

./led_test & //启动B进程

ps //查看A,B进程的PID

kill B进程

kill B进程

ps

top

kill A进程

 

 

 

 

 

 

 

 

 

 

 

 

 

 

#include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <avr/eeprom.h> #include <string.h> #define delay_ms(x) _delay_ms(x) // LCD 相关引脚定义 #define LCD_RS PC0 #define LCD_RW PC1 #define LCD_E PC2 #define LCD_D4 PA4 #define LCD_D5 PA5 #define LCD_D6 PA6 #define LCD_D7 PA7 // 按键引脚定义(使用PORTD) #define SET_BUTTON PIND0 #define ADD_BUTTON PIND1 #define SUB_BUTTON PIND2 // LED 引脚定义 #define LED_PIN PB3 // EEPROM存储地址 #define EEPROM_TIME_HOUR_ADDR 0x00 #define EEPROM_TIME_MINUTE_ADDR 0x01 #define EEPROM_DATE_YEAR_ADDR 0x02 #define EEPROM_DATE_MONTH_ADDR 0x03 #define EEPROM_DATE_DAY_ADDR 0x04 // 全局变量 volatile uint8_t hour = 12; // 小时(0-23) volatile uint8_t minute = 0; // 分钟(0-59) volatile uint8_t second = 0; // 秒(0-59) volatile uint8_t year = 23; // 年份(后两位) volatile uint8_t month = 1; // 月份(1-12) volatile uint8_t day = 1; // 日期(1-31) volatile uint8_t set_mode = 0; // 0:正常显示, 1:设置小时, 2:设置分钟, 3:设置年, 4:设置月, 5:设置日 volatile uint8_t blink_state = 0; // 闪烁状态 volatile uint8_t timer1_counter = 0; // 定时器计数器 // 闹钟结构 typedef struct { uint8_t hour; uint8_t minute; uint8_t enabled; } Alarm; Alarm alarm1 = {8, 0, 0}; // 闹钟1,默认8:00,关闭 Alarm alarm2 = {12, 0, 0}; // 闹钟2,默认12:00,关闭 // LCD初始化 void LCD_Init() { // 设置数据端口为输出 DDRA |= (1<<LCD_D4) | (1<<LCD_D5) | (1<<LCD_D6) | (1<<LCD_D7); // 设置控制端口为输出 DDRC |= (1<<LCD_RS) | (1<<LCD_RW) | (1<<LCD_E); // 初始化序列 delay_ms(50); // 等待LCD上电稳定 // 4位模式初始化 LCD_Write_Command(0x33); LCD_Write_Command(0x32); LCD_Write_Command(0x28); // 4位模式,2行显示,5x8点阵 LCD_Write_Command(0x0C); // 显示开,光标关,闪烁关 LCD_Write_Command(0x06); // 增量模式,不移位 LCD_Write_Command(0x01); // 清屏 delay_ms(2); } // 向LCD发送命令 void LCD_Write_Command(unsigned char cmd) { PORTC &= ~(1<<LCD_RS); // RS=0 命令模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (cmd & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((cmd << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 向LCD发送数据 void LCD_Write_Data(unsigned char data) { PORTC |= (1<<LCD_RS); // RS=1 数据模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (data & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((data << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 在LCD上显示字符串 void LCD_Display_String(char *str) { while (*str) { LCD_Write_Data(*str++); } } // 设置LCD光标位置 void LCD_Set_Cursor(uint8_t row, uint8_t col) { uint8_t address; if (row == 0) { address = 0x80 + col; } else { address = 0xC0 + col; } LCD_Write_Command(address); } // 显示时间 void Display_Time() { char buffer[16]; // 第一行显示时间 LCD_Set_Cursor(0, 4); sprintf(buffer, "%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); // 第二行显示日期 LCD_Set_Cursor(1, 4); sprintf(buffer, "20%02d-%02d-%02d", year, month, day); LCD_Display_String(buffer); } // 显示设置模式 void Display_Set_Mode() { char buffer[16]; LCD_Set_Cursor(0, 0); LCD_Display_String("Set:"); switch(set_mode) { case 1: // 设置小时 if(blink_state) { sprintf(buffer, "Hour: %02d", hour); } else { sprintf(buffer, "Hour: "); } break; case 2: // 设置分钟 if(blink_state) { sprintf(buffer, "Minute:%02d", minute); } else { sprintf(buffer, "Minute: "); } break; case 3: // 设置年 if(blink_state) { sprintf(buffer, "Year: 20%02d", year); } else { sprintf(buffer, "Year: "); } break; case 4: // 设置月 if(blink_state) { sprintf(buffer, "Month: %02d", month); } else { sprintf(buffer, "Month: "); } break; case 5: // 设置日 if(blink_state) { sprintf(buffer, "Day: %02d", day); } else { sprintf(buffer, "Day: "); } break; default: sprintf(buffer, "Normal Mode "); } LCD_Set_Cursor(0, 5); LCD_Display_String(buffer); // 仍然显示时间日期 LCD_Set_Cursor(1, 0); sprintf(buffer, "Time:%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); } // 从EEPROM加载时间日期 void Load_Time_From_EEPROM() { hour = eeprom_read_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR); minute = eeprom_read_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR); year = eeprom_read_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR); month = eeprom_read_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR); day = eeprom_read_byte((uint8_t*)EEPROM_DATE_DAY_ADDR); // 检查读取的值是否有效 if(hour > 23) hour = 0; if(minute > 59) minute = 0; if(year > 99) year = 23; if(month == 0 || month > 12) month = 1; if(day == 0 || day > 31) day = 1; } // 保存时间日期到EEPROM void Save_Time_To_EEPROM() { eeprom_update_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR, hour); eeprom_update_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR, minute); eeprom_update_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR, year); eeprom_update_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR, month); eeprom_update_byte((uint8_t*)EEPROM_DATE_DAY_ADDR, day); } // 初始化定时器1 (1秒中断) void Timer1_Init() { // 设置定时器1为CTC模式 TCCR1B |= (1 << WGM12); // 设置预分频为1024 TCCR1B |= (1 << CS12) | (1 << CS10); // 设置比较值 (16MHz/1024 = 15625 ticks/sec, 15625 ticks = 1秒) OCR1A = 15625; // 启用比较匹配中断 TIMSK |= (1 << OCIE1A); } // 初始化按键引脚 void Buttons_Init() { // 设置按键引脚为输入,启用上拉电阻 DDRD &= ~((1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON)); PORTD |= (1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON); } // 按键扫描 void Key_Scan() { static uint8_t last_set_state = 1; static uint8_t last_add_state = 1; static uint8_t last_sub_state = 1; uint8_t current_set_state = PIND & (1<<SET_BUTTON); uint8_t current_add_state = PIND & (1<<ADD_BUTTON); uint8_t current_sub_state = PIND & (1<<SUB_BUTTON); // 检测设置按键按下 if(last_set_state && !current_set_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SET_BUTTON))) { set_mode++; if(set_mode > 5) set_mode = 0; if(set_mode == 0) { // 退出设置模式,保存时间到EEPROM Save_Time_To_EEPROM(); } } } // 只在设置模式下检测加减按键 if(set_mode > 0) { // 检测增加按键按下 if(last_add_state && !current_add_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<ADD_BUTTON))) { switch(set_mode) { case 1: // 增加小时 hour = (hour + 1) % 24; break; case 2: // 增加分钟 minute = (minute + 1) % 60; break; case 3: // 增加年 year = (year + 1) % 100; break; case 4: // 增加月 month = (month % 12) + 1; break; case 5: // 增加日 day = (day % 31) + 1; break; } } } // 检测减少按键按下 if(last_sub_state && !current_sub_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SUB_BUTTON))) { switch(set_mode) { case 1: // 减少小时 hour = (hour == 0) ? 23 : hour - 1; break; case 2: // 减少分钟 minute = (minute == 0) ? 59 : minute - 1; break; case 3: // 减少年 year = (year == 0) ? 99 : year - 1; break; case 4: // 减少月 month = (month == 1) ? 12 : month - 1; break; case 5: // 减少日 day = (day == 1) ? 31 : day - 1; break; } } } } last_set_state = current_set_state; last_add_state = current_add_state; last_sub_state = current_sub_state; } // 检查闹钟 void Check_Alarms() { static uint8_t alarm1_triggered = 0; static uint8_t alarm2_triggered = 0; // 检查闹钟1 if(alarm1.enabled && !alarm1_triggered && hour == alarm1.hour && minute == alarm1.minute && second == 0) { alarm1_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 检查闹钟2 if(alarm2.enabled && !alarm2_triggered && hour == alarm2.hour && minute == alarm2.minute && second == 0) { alarm2_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 每分钟重置闹钟触发标志 if(second == 0) { alarm1_triggered = 0; alarm2_triggered = 0; } // 如果LED亮起,检测按键关闭 if(PORTB & (1<<LED_PIN)) { if(!(PIND & (1<<SET_BUTTON)) || !(PIND & (1<<ADD_BUTTON)) || !(PIND & (1<<SUB_BUTTON))) { PORTB &= ~(1<<LED_PIN); // 关闭LED } } } // 定时器1比较匹配中断服务程序 ISR(TIMER1_COMPA_vect) { // 更新时间 second++; if(second >= 60) { second = 0; minute++; if(minute >= 60) { minute = 0; hour++; if(hour >= 24) { hour = 0; // 日期增加逻辑 day++; uint8_t max_day = 31; // 处理不同月份的天数 if(month == 4 || month == 6 || month == 9 || month == 11) { max_day = 30; } else if(month == 2) { // 简单处理2月天数(不考虑闰年) max_day = 28; } if(day > max_day) { day = 1; month++; if(month > 12) { month = 1; year++; if(year > 99) year = 0; } } } } } // 更新闪烁状态 (0.5秒周期) timer1_counter++; if(timer1_counter >= 5) { // 10次 = 1秒 (0.1秒中断) timer1_counter = 0; blink_state = !blink_state; } // 检查闹钟 Check_Alarms(); } int main(void) { // 初始化端口 DDRB |= (1<<LED_PIN); // LED引脚为输出 PORTB &= ~(1<<LED_PIN); // 初始关闭LED // 初始化按键 Buttons_Init(); // 初始化LCD LCD_Init(); // 从EEPROM加载时间 Load_Time_From_EEPROM(); // 初始化定时器 Timer1_Init(); // 启用全局中断 sei(); // 清屏并显示欢迎信息 LCD_Write_Command(0x01); LCD_Set_Cursor(0, 3); LCD_Display_String("AVR Clock"); LCD_Set_Cursor(1, 2); LCD_Display_String("Initializing..."); delay_ms(1000); LCD_Write_Command(0x01); while(1) { // 扫描按键 Key_Scan(); // 更新显示 if(set_mode == 0) { Display_Time(); } else { Display_Set_Mode(); } // 短暂延迟 delay_ms(100); } return 0; }使用的atmega128单片机,不改动引脚的使用
05-27
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值