drivers_day13

本文深入探讨了Linux内核分离思想在驱动开发中的应用,详细阐述了平台总线、平台设备和平台驱动的概念及使用方法。重点分析了如何通过ioremap实现LED开关驱动,并解释了内存映射的过程。此外,文章还讨论了应用程序如何通过系统调用访问硬件设备,以及在不同场景下数据拷贝的影响。最后,介绍了如何利用mmap将硬件设备的物理地址映射到用户虚拟地址空间,以减少数据拷贝次数。

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

回顾:

1.linux内核分离思想

明确:一个完整的硬件设备驱动必须包含硬件信息和软件信息;

分离:就是将硬件信息和软件信息进行分离,让以后驱动的开发重心放在硬件信息上,让驱动开发者从软件的实现上解脱出来!

linux内核分离思想的实现:

定义一个平台总线platform_bus_type(struct bus_type虚拟总线类型)来维护两个链表。

特点:

内核已经实现;

维护dev链表和drv链表;

前者保存硬件信息,后者保存软件信息;

match函数:用于匹配;

驱动只需关注一下两个结构体:

平台设备:struct platform_device

描述装载硬件信息

驱动实现;

内核一般实现,会将platform_device的硬件信息在平台代码中实现!arch/arm/mach-s5pv210/mach-cw210.c

使用方法:

1.分配初始化struct platform_device

struct platform_devicexxx_dev = {

.name = 必须要指定,用于匹配

.id = 设备编号

.resource = 装载硬件资源//一般是建立一个struct  resource 类型的数组,将数组名赋给resource

.num_resources = resource类型的硬件资源的个数,也就是数组的元素的个数

.dev = {

.platform_data = 装载自己定义描述硬件的结构体(led_resource,btn_resource)

.release=led_dev,//必须写,否则会报错!void (*release)(struct device*)

}

};

platform_device_register//向内核注册硬件信息

1.添加硬件节点到dev链表中

2.遍历drv链表,进行匹配,如果匹配成功,调用匹配成功软件节点的probe函数,然后将硬件节点的首地址传递给probe函数

platform_device_unregister //卸载硬件信息

平台驱动:struct platform_driver

描述装载软件信息;

在驱动中完成,一般不会在平台代码中完成;

1.分配初始化platform_driver软件节点

struct platform_driverxxx_drv = {

.driver = {

.name = 必须指定,用于匹配

},

.probe = 硬件和软件匹配成功内核调用

.remove = 卸载硬件或者卸载软件内核调用

};

2.注册

platform_driver_register

卸载

platform_driver_unregister

注意:

platform_device和platform_driver之间的连线者是name字段!

probe函数是否被执行代表着软件和硬件是否匹配成功,也代表着一个驱动的完整性,标志驱动生命周期的开始!

platform_device装载硬件信息的方法:

方法1:struct resource来装载硬件信息;给resource字段使用

方法2:自己定义硬件的数据结构体struct led_resource,给dev字段的platform_data成员使用

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

Day13

案例:分析ioremap实现LED开关驱动

分析:

1.明确:不管是在用户空间还是在内核空间,软件一律不能去直接访问设备的物理地址;

在内核空间通过iomap函数将物理地址映射成内核的虚拟地址(动态内存映射区),在用户空间将通过系统调用,通过内核来访问硬件!

2.在内核驱动中如果要访问设备的物理地址,需要利用ioremap将设备的物理地址映射到内核虚拟地址上(动态内存映射区),以后驱动程序访问这个内核虚拟地址就是在间接得访问设备的物理地址(MMU内存管理单元,TLB(cache的一个硬件单元,cache从TLB中取页表,没有则TLB从内存中取出,再给cache),TTW)

3.如果用户要访问硬件设备,不能直接访问,也不能在用户空间访问,只能通过系统调用(open,close,read,write,ioctl)来访问映射好的内核虚拟地址,通过这种间接的访问来访问硬件设备,但是如果涉及到数据的拷贝,还需要借助4个内存拷贝函数!Cpoy_from_user/copy_to_user/get_user/put_user

结论:

1.通过以上的分析,发现应用程序通过read,write,ioctl来访问硬件设备,它们都要经过两次的数据拷贝,一次是用户空间和内核空间的数据拷贝,另外一次是内核空间和硬件之间的数据拷贝,如果设备拷贝的数据量比较小,那么read,write,ioctl的两次数据拷贝的过程对系统的影响几乎可以忽略不计,如果设备的数据量非常大,例如显卡(独立),LCD屏幕(显存共享主存)(独立显卡有自己的显存,集成显卡没有自己的显存,共享主存,因此速度慢,效率低),摄像头,声卡这类设备涉及的数据量比较庞大,如果还是用read,write,ioctl进行访问设备数据,无形对系统的性能影响非常大。

类似于搬家:一个人的搬家,东西少,很轻松;一个家庭的搬家东西很多,很麻烦!

2.用户访问设备,最终其实涉及的用户和硬件,而read,write,ioctl本身会牵扯到内核,所以这些函数涉及2次的数据拷贝,用户要直接去访问硬件设备,只需要将硬件设备的物理地址信息映射到用户的虚拟地址空间即可,一旦完毕,不会在牵扯到内核空间,以后用户直接访问用户的虚拟地址就是在访问设备硬件,由2次的数据拷贝的转换为一次的数据拷贝。

目的:将硬件物理地址映射到用户虚拟地址空间,由2次数据拷贝变成1次数据拷贝!

3.如何实现将硬件设备的物理地址映射到用户空间的虚拟内存上呢?

用户空间3G虚拟内存区域的划分:

高地址开始:

栈区

MMAP内存映射区

堆区

BSS段区

DATA段区

TEXT段区

MMAP内存映射区作用:

应用程序使用的动态库映射到这个区域;

应用程序调用mmap,将设备物理地址和这个区域的虚拟内存进行映射;

结论:linux系统通过mmap来实现将物理地址映射到用户3G的MMAP内存映射区上的虚拟内存上!

mmap系统调用的过程:

void *addr;

addr = mmap(0,0x1000,PROT_READ|PROT_WRITE,

MAP_SHARED, fd, 0);

1.应用程序调用mmap首先调用C库的mmap

2.C库的mmap保存mmap的系统调用号到R7中,然后调用svc触发软中断异常(陷入内核空间)

3.内核启动时,已经初始化好了异常向量表,触发软中断,跳转到软中断的异常向量表的入口地址vector_swi.

4.根据R7保存的系统调用号,以它索引,在内核的系统调用表找到对应的函数sys_mmap,然后调用内核实现的sys_mmap

5.sys_mmap内核会做两件事:

1.首先在当前进程的MMAP内存映射区中找一块空闲的虚拟内存区域;

2.一旦找到以后,利用struct vm_area_strcut结构创建一个对象来描述这块空闲的虚拟内存区域

6.sys_mmap最终调用底层驱动的mmap,然后将描述空闲虚拟内存区域的对象指针传递给底层驱动的mmap函数使用;

7.底层驱动的mmap根据传递过来的虚拟内存区域的信息获取用户要映射的虚拟地址,再根据某些函数建立用户虚拟地址和物理地址的映射关系

8.一旦建立映射,mmap函数返回,返回值保存着这块空闲内存区域的起始地址,以后用户在用户空间就可以为所欲为了!

struct vm_area_struct {

unsigned long vm_start;//空闲虚拟内存的起始地址

unsigned long vm_end;//结束地址

pgprot_t vm_page_prot; //访问权限

unsigned long vm_pgoff;//偏移量

};

9.驱动mmap利用一下函数建立映射(用户虚拟地址和物理地址)

intremap_pfn_range(struct vm_area_struct *vma,unsigned long addr, unsigned longpfn, unsigned size,pgprot_t prot);

vma: 用户虚拟内存区域指针

addr: 用户虚拟内存起始地址->vma->vm_start

pfn: 要映射的物理地址所在页帧号,可以通过物理地址>>12得到

size: 待映射的内存区域的大小

prot: vma的保护属性vma->vm_page_prot

功能:建立已知的用户虚拟内存和已知的物理地址之间的映射关系;

注意:利用这个函数进行地址映射的时候,不管是物理地址还是用户虚拟地址都要求是页的整数倍!

1页=4K=0x1000

0xe0200080这个GPIO寄存器地址不是页的整数倍!

通过芯片手册可知GPIO使用的地址空间范围:

0xE0200000 ~ 0xE02FFFFF

映射时指定的物理地址应该是:0xE0200000(页的整数倍)

访问0xe0200080:用户虚拟地址 + 0x80

访问0xe0200084:用户虚拟地址+ 0x84

注意:一个物理地址同时可以映射到内核的虚拟地址上,还可以映射到用户的虚拟地址上!

案例:利用mmap来实现点灯。采用分离思想来实现

C,B位:NCNB,NCB,CNB,CB

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

案例:一个应用程序如何去处理多个设备,例如应用程序读取网路数据,按键,串口

分析:

明确:对设备访问永远先open

int fdx = open

读取设备的数据:

方法1:

串行+阻塞的方式读取:

while(1) {

read(标准输入);

read(网络);

}

缺点:每当阻塞读取标准输入时,如果用户不进行标准输入的操作,而此时客户端给服务器发送数据,导致服务器无法读取客户端发送来的数据!

方法2:

采用多线程或者多进程机制来实现读取:

开辟多个线程,每一个线程处理一个设备,不会导致的数据的无法读取,但是系统的开销相比方法1要大!

方案3:采用linux系统提供的高级IO的处理机制

select/poll:两者一样,主进程能够利用select或者poll能够对多个设备进行监听!

linux关于select的应用编程方法:man select

select函数原型:

int select(int nfds,

fd_set *readfds,

fd_set *writefds,

fd_set *exceptfds,

struct timeval *timeout);

函数功能:

主进程利用此函数能够对多个设备进行监听,一旦发现监听的设备都不可用(不可读也不可写也没有异常),那么主进程进入休眠状态,一旦监听的设备中,只要有一个设备可用(可读或者可写或者有异常)都会唤醒休眠的主进程,select也就会返回。

注意这个函数仅仅起到一个监听的功能,数据的后续处理,例如读,写都是通过read,write,ioctl来进行!

参数说明:

nfds:

对设备的访问永远先open获取fd;

监听的设备中,最大的文件描述符fd+1;

数据类型fd_set:文件描述符集合,用来保存描述监听的设备,里面存放是被监听设备的文件描述符;如果select要监听某一个设备,必须把这个设备的fd添加到对应的文件描述符集合中!

readfds:读文件描述符集合指针,如果select要监听设备是否可读,需将设备的fd添加到这个集合中!

writefds:写文件描述符集合指针,如果select要监听设备是否可写,需将设备的fd添加到这个集合中!

exceptfds:异常文件描述符集合指针,如果select要监听设备是否有异常,需将设备的fd添加到这个集合中!

注意:一个设备的fd可以同时添加到三个集合中!

timeout:指定监听的超时时间,如果此参数指定了一个时间,例如5秒钟,select发现设备不可用,主进程进入休眠状态,如果5秒之内设备还不可用,5秒到期,主进程主动唤醒;如果此参数指定为NULL,休眠为永久休眠!

返回值:有三种

如果等于0:表明是超时;

如果小于0:表明系统出错;

如果大于0:表明设备可用(至少是一个设备,或者全部);

文件描述符集合操作的方法:

fd_set rfds; //定义读文件描述符集合

//从集合中解除对fd设备的监听

void FD_CLR(int fd, fd_set *set);

//判断是否是设备fd引起的主进程的唤醒,如果是返回true,否则返回false

int FD_ISSET(int fd, fd_set *set);

//添加一个新的被监听的设备

void FD_SET(int fd, fd_set *set);

//清空文件描述符集合

void FD_ZERO(fd_set *set);

注意:如果要重复监听,需要再次清空集合和添加监听设备!

案例:采用select监听网络和标准输入

下载5.0代码

实验步骤:

PC机:

1.make

2.cp udplib_test/opt/rootfs/home/apptest/

3.cp libudp.so /opt/rootfs/home/applib/

注意修改/opt/rootfs/etc/profile添加库的路径:

exportLD_LIBRARY_PATH=/home/applib:$LD_LIBRARY_PATH

ARM:

运行服务器端:

/home/apptest/udplib_test

PC:

运行客户端

. env.sh //注意env前加空格

./client

 

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

余额充值