drivers_day07

本文详细介绍了中断编程的基本原理及其在Linux内核中的实现方法。包括中断的作用、硬件触发过程、处理流程等基础知识,并重点讲解了如何通过顶半部和底半部处理来优化中断处理函数,提高系统的并发性和响应能力。

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

回顾:

中断

1.为什么有中断,中断作用?

因为CPU与外设处理数据的速度不同,CPU处理数据的速度远远大于外设处理数据的速度!以串口为例:波特率115200bits/s,但CPU为1G的主频(109),如果采用轮询的方式,则每隔一定时间,查看一次。在这段时间内只能干一件事!CPU的利用率太低!

轮询的方式: 轮询(Polling)是一种CPU决策如何提供周边设备服务的方式,又称“程控输出入”(Programmed I/O)。轮询法的概念是,由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。

中断的方式

2.中断的硬件触发的过程

中断硬件控制器:开/关中断、设置中断优先级、

3.中断的处理流程

画出处理流程图:源程序、中断请求、中断响应

异常向量表

保护现场

恢复现场

4.linux内核中断编程

不管是什么体系结构,是否有操作系统,中断编程都遵循如下处理过程:

1.建立异常向量表

2.编写保护现场的代码

3.执行中断服务程序

   根据中断控制器来判断是哪个中断,然后处理这个中断对应的服务程序

4.编写恢复现场的代码

 

linux内核中断的实现:                                          

明确:linux内核中断处理还是遵循以上的规则,只是从驱动编程的角度来说,只需关注一点即可:

1.异常向量表,保护现场,恢复现场的代码都已经由linux内核来实现;

2.驱动开发人员只需向内核申请硬件中断资源和注册这个硬件中断对应的中断处理函数即可。

 

linux内核中断编程:

申请中断资源和注册中断对应的服务程序:

request_irq(中断号,中断处理函数,中断标志,中断名称,给中断处理函数传递的参数);

中断资源一旦不再使用,一定将资源归还给内核,然后删除对应的服务程序。

free_irq(中断号,给中断处理函数传递的参数);

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

linux内核中断处理函数编程要求:

1.明确之前所说的硬件中断优先级仅仅适用于中断控制器

2.linux内核对于硬件中断无优先级这个概念,明确linux内核硬件中断优先级高于软中断的优先级,软中断的优先级高于进程

软中断分优先级(有2个优先级),进程也有优先级。

3.就是因为linux内核对于硬件中断无优先级,所以要求中断处理函数的执行速度要快,让中断及时释放CPU资源给别的中断或者进程使用。如果中断处理函数长时间的占有CPU资源,别的硬件中断或者进程,软中断无法获取CPU资源,影响系统的并发能力和响应能力!

4.在中断处理函数中千万不能调用引起阻塞(忙等待或者休眠等待)的函数,例如copy_to_user,copy_from_user,kmalloc

5.中断不属于任何进程,不参与进程的调度(实时linux内核将中断线程化)

6.中断处理函数中不能和用户进程进行数据的交互,如果要进行数据交互,一定要配合使用系统调用(struct file_operations)!

7.linux内核指定的中断栈为1页,早起内核中断栈共享进程栈

 

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

共享中断:多个硬件外设共享一个硬件中断资源。

注意:

有多个硬件外设,意味着有多个设备驱动,有多个设备驱动意味着每一个驱动程序都会调用reqeust_irq注册中断。

共享中断的编程要求:

1.必须指定IRQF_SHARED;中断标志采用共享和其他的触发方式(例如上升沿触发);IRQF_SHARED|IRQF_TRIGGER_RISING

2.dev_id必须不一样;给中断处理函数传递的参数必须不一样,因为释放中断资源时,要给中断处理函数传递参数!

3.中断处理函数不能调用disable_irq(关闭中断);关闭了中断,共享该中断的设备就不能响应中断了!

4.CPU区分外设产生中断前提是外设必须硬件上具备判断中断是否是其产生的条件!外设硬件不具备这个条件,这个外设不能使用共享中断!

实际开发中:共享中断基本不用!一个设备一个中断!不会多个设备共用一个中断!

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

问题:移植tslib和QT源码,触摸屏的设备文件:

hexdump /dev/input/event0 ,点击屏幕,是否有打印信息,如果没有,在执行一下命令

hexdump /dev/input/event1

别忘记修改/opt/rootfs/etc/profile重新设置环境变量

 

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

问题:我的中断处理函数就是执行的时间非常长,怎么办?

答:

明确:如果中断处理函数长时间的占用CPU资源,会导致别的任务(指的是硬件中断、软中断和进程)无法获取CPU资源,影响系统的并发能力和响应能力。

甚至如果在中断处理函数中进行休眠操作,最终导致linux系统处于僵死状态!

结论:

linux内核为了提高系统的并发能力(进程的)响应能力(中断),解决中断处理函数长时间的占有CPU的情况,linux内核将中断处理函数进行划分,划分为两部分:顶半部,底半部。

注意这种划分不是函数的划分和函数之间的调用!

划分之前的中断处理函数=顶半部内容+底半部内容!

顶半部:本质上还是之前的中断处理函数,其中完成的内容相对比较紧急,耗时较短,遵循linux内核要求中断处理函数执行的速度要快这个原则,一旦中断发生以后,内核首先执行顶半部内容,但是这个顶半部占用CPU的时间非常短,也就保证其他任务可以及时获取到CPU的资源。其他复杂的事情可以放在底半部去完成。顶半部还需要登记底半部告诉CPU我的中断还需要一些比较耗时的内容在将来(空闲时)要去你去完成!顶半部不可被中断!

 

底半部:完成之前中断处理函数中比较耗时,不紧急的事情,可以被别的中断(硬件中断和软件中断,甚至是进程)打断(当底半部采用tasklet机制时,只能被硬件中断打断,因为它是基于软中断的,而软中断的优先级高于进程,当采用工作队列时,则会被硬件中断、软中断、进程所打断)!底半部的执行会在CPU空闲的时候去完成!

 

问:底半部如何实现?

答:三种实现机制:

1.tasklet

2.工作队列

3.软中断

它们都是延后执行的机制!这些机制不仅可以用于中断,还可以用于其它的方面!只要有延时需要的,都可以采用这些机制!例如:可以使用工作队列的方式,控制led灯每隔2秒开关!

 

(1)tasklet:又名“小任务”,任务说的是软中断,tasklet也是基于软中断实现,优先级高于进程,运行在中断上下文中,可以被硬件中断打断,不能被进程打断,具有内核中对中断处理函数的要求!处理函数执行速度快,及时释放资源给其它任务!不能调用引起阻塞的函数或者休眠!

linux内核描述tasklet使用的数据结构:

struct tasklet_struct

{

       void(*func)(unsigned long); //底半部处理函数

       unsignedlong data;//给底半部处理函数传递的参数,一般传递指针,要注意在处理函数中对数据类型的转换

};

 

如何使用呢?

1.分配初始化tasklet对象

   方法1:

  DECLARE_TASKLET(tasklet变量名,tasklet处理函数,给处理函数传递的参数);

   方法2:

  struct tasklet_struct tasklet; //分配

  tasklet_init(&tasklet, 处理函数,给处理函数传递的参数); //初始化

2.在顶半部(中断处理函数)中调用tasklet_schedule函数进行登记底半部tasklet,是登记而不是执行!一旦登记成功,顶半部肯定先执行完毕,赶紧释放CPU资源,tasklet的处理函数会在CPU空闲时去执行。

 

3.注意事项:tasklet还是工作在中断上下文中,遵循中断的处理过程,千万不能做休眠阻塞的事情!

 

4.tasklet就是将中断处理函数中比较耗时的内容进行了延后执行

 

案例:将按键中断采用tasklet来实现。

 

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

问题:底半部就需要去休眠,怎么办?

答:tasklet的延后处理函数不允许休眠,但是在某些场合可能需要进行休眠操作,又要延后执行,这时可以考虑使用工作队列。工作队列相关的延后处理函数允许休眠。

明确:“休眠”这个词仅仅适用于进程!硬件中断、软中断不可以休眠,因为中断休眠会导致僵死状态,什么事都干不了!

工作队列:工作在进程上下文中,允许和进程一样重新调度甚至休眠。但是tasklet不允许这么做!

 

工作队列实现过程:

1.工作队列延后执行涉及的数据结构

  struct work_struct {

  work_func_t function; //工作延后处理函数,一旦CPU空闲,CPU就会执行这个处理函数

};

 

  struct delayed_work{

       structwork_struct work; //用来包含工作延后处理函数

       structtimer_list timer;//用来指定执行时间间隔

  };

2.如何使用工作队列来进行延后处理?

分配工作或者延后工作对象

struct work_struct work; //分配一个普通的工作

或者

struct delayed_work dwork;//分配一个延时的工作

 

初始化工作或者延时工作

INIT_WORK(&work, work_function);//初始化普通的工作,并且指定工作的延后处理函数work_function

INIT_DELAYED_WORK(&dwork, dwork_function);//初始化延时的工作,并且指定延时工作的处理函数dwork_fuction

 

在顶半部(中断处理函数)中登记普通的工作或者延时工作:

schedule_work(&work); //一旦登记,CPU在空闲时立即执行普通工作的处理函数

或者

schedule_delayed_work(&dwork, 5*HZ);//将延时工作的登记放在5秒以后去登记,一旦登记完毕,CPU在空闲时立即执行延后工作的处理函数。

 

注意:工作队列工作在进程上下文中,允许休眠,但是它的优先级要低于tasklet(工作在中断上下文中)

 

案例:利用工作队列实现按键驱动

案例:利用工作队列每隔2秒开关灯

 

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

总结:

调用schedule_work或者schedule_delayed_work这两个函数,都会将工作和延时工作交给内核默认的工作队列和内核线程,内核默认的线程叫[events/0]...[events/CPU编号],这种用法的优点是简单易用,程序员不需要关心如何创建工作队列和内核线程,但是缺点是导致内核的线程的负载过程,执行的效率太低!可以考虑创建自己的工作队列和内核线程去处理自己的工作或者延时工作。

 

linux内核描述工作队列的数据结构:

struct workqueue_struct;//里面存放的是登记的工作或者延时工作。

 

如何创建自己的工作队列和内核线程?

struct workqueue_struct * create_workqueue(char *name);

函数功能:

会给每一个CPU都创建自己的工作队列和内核线程,并且将自己的工作队列和内核线程进行绑定,以后自己的内核线程只处理自己工作队列上的工作或者延时工作。

返回值:创建的工作队列的指针

参数name:创建的内核线程的名字,通过ps命令查看

create_singlethread_workqueue:这个函数仅仅创建一个内核线程或者工作队列,它没有和具体的CPU进行绑定,在使用的时候可以指定到具体的某个CPU上去!

 

如何将自己的工作或者延时工作交给自己的内核线程去处理呢?

queue_work(自己创建的工作队列,自己的工作);

queue_delayed_work(自己的工作队列,自己的延时工作,登记的延时间隔);

注意跟schedule_work和schedule_delayed_work区别

 

销毁自己的工作队列和内核线程:

destroy_workqueue(自己的工作队列指针);

 

案例:优化按键驱动,创建自己的工作队列和内核线程处理按键中断。

 

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

软中断:

软中断对应的延后处理函数运行在中断上下文中,tasklet本身也是基于软中断实现的,它和tasklet之间的区别:

1.软中断的延后处理函数可以同时在多个CPU上同时执行!但是tasklet不行,只能在一个CPU上运行。

2.软中断的处理函数在设计的时候必须具备可重入性。

函数一:

static int g_data; //全局变量

void swap(int *x, int *y)

{

       g_data= *x;

       *x= *y;

       *y= g_data;

}

函数二:

void swap(int *x, int *y)

{

       intdata;

       data= *x;

       *x= *y;

       *y= data;      

}

如何将一个函数设计为可重入函数:

1.尽量避免使用全局变量

2.如果使用全局变量,一定要进行互斥访问,比如加锁,或者关闭中断

 

3.软中断的处理函数不能以模块的形式实现,必须修改内核源码,静态编译内核源码

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

#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、付费专栏及课程。

余额充值