drivers_day14

本文深入讲解了Linux环境下ADC驱动的设计与实现,包括ADC的基本概念、硬件特性、寄存器配置及中断处理等内容,并探讨了如何利用混杂设备驱动实现ADC功能。

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

回顾:

1.mmap:

作用:就是将设备的物理地址映射到用户的虚拟内存空间上;

read,write,ioctl三个系统调用函数,如果涉及数据的访问,必然要经过两次的数据拷贝:用户空间,内核空间,硬件。

mmap的使用将2次的数据操作变成1次,大大的提高了设备数据的访问效率!

mmap系统调用过程:

1.应用程序mmap

2.C库的mmap

3.sys_mmap:

内核到当前进程的3G的MMAP内存映射区中找一块空闲的内存区域;一旦找到,内核使用struct vm_area_struct来描述这块空闲的内存区域的信息;

4.调用底层驱动的mmap

5.remap_pfn_range进行将物理地址映射到内核帮你找的虚拟内存进行地址映射!

 

2.linux应用编程高级IO:IO多路监听select/poll

案例:一个应用程序访问多个设备

方案1:串行

方法2:多线程

方案3:select.

 

select函数的功能:

利用此函数能够监听多个设备,监听设备是否可读,是否可写,或者是否有异常,如果监听的设备都不可用(即不可读也不可写也无异常),那么select就会让主进程进入休眠状态;监听的设备中只要有一个设备(或者监听的设备)可用(可读或者可写或者有异常)都会唤醒休眠的进程;

 

分析以上的总结内容:

1.多个设备就代表着有多个驱动程序;每一个设备都对应一个驱动;

2.设备不可用代表着应用程序会到内核空间操作硬件设备,只有在内核空间才有权限访问硬件设备;

3.主进程休眠代表主进程进入内核空间进行休眠,在内核空间必将利用等待队列机制让主进程休眠;

4.等待队列需要有一个等待队列头,唤醒休眠进程,使用wake_up,这个函数只需传递一个等待队列头即可唤醒休眠进程;只要有一个设备可用都会唤醒休眠的进程,这句话的潜台词是说一个主进程分别被添加到被监听的设备对应的驱动程序定义的等待队列头中!

5.select函数引起的主进程休眠,假如底层驱动也有对应的select函数,那只需要利用等待队列机制让主进程在底层驱动的select函数进行休眠操作即可!

 

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

select系统调用过程:

1.应用程序调用select,首先调用C库的select函数实现;

2.C库的select保存select系统调用号到R7,调用SVC或者SWI触发软中断,至此由用户空间陷入内核空间,ARM的工作模式由用户模式转变为SVC管理模式;

3.跳转到内核准备好的异常向量表的入口地址,根据R7保存的系统调用,以它为索引在系统调用表中找到对应的实现函数sys_select

4.sys_select要完成:

1.把被监听的设备对应驱动程序的poll函数挨个调用一遍,

被监听的设备都不可用,它们的驱动的poll函数都返回0;

2.判断是否是驱动主动唤醒,还是超时唤醒,还是接收到信号唤醒;

3.如果即没有驱动主动唤醒,也没有超时唤醒,没有接收到信号,sys_select调用poll_schedule_timeout主动让进程进入休眠

4.假设被监听的设备中,有一个设备可用(可读或者可写或者异常,硬件通过中断来判断),都会唤醒休眠的主进程;

5.sys_select的poll_schedule_timeout函数返回,不再休眠

6.再次把被监听的设备驱动的poll函数挨个调用一遍,此时可用的设备对应的驱动poll函数会返回非0;

7.if (ret || time_out || ...) //ret = 1,立即返回到用户空间,

返回值为ret值

 

总结:

1.明确本来应该底层驱动的poll函数利用等待队列机制让进程休眠,但是等待队列休眠9步骤并不都是驱动的poll来编写,有一部分是有内核sys_select来实现;

 

2.驱动poll函数完成如下内容即可:

1.调用poll_wait,将当前进程添加到驱动定义的等待队列头中

2.根据设备是否可用,决定返回0还是非0

 

3.明确:监听机制,底层poll函数不是必须的,如果要监听设备还可以使用多线程机制也能够完成监听;但是如果要使用select/poll监听设备,驱动必须有poll实现!

 

案例:给之前的按键驱动添加被监听机制,并且采用平台总线的软件实现!

 

linux下,有多个文件时,调用多个文件中的函数时!

ctags 看源码:

ctags -R * //做源码数据库,生成tags文件(将该目录下的所有文件做成源码数据库,生成tags文件)

vim btn_drv.c //tags文件在哪个目录下,就需要在哪个目录进行打开文件操作

 

如何实现跳转(如何查看调用的另一个文件中的函数或者结构体、变量的实现):

1.光标移动到跳转的地方(函数的调用处)

2.ctrl + ] (右方括号)跳转

3.ctrl + t 返回

 

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

linux内核混杂设备:

1.混杂设备:主设备号已经被内核进行分配,主设备号为10,通过次设备号区分的一类字符设备!

特点:

还是字符设备

主设备号为10

通过次设备号来区分设备个体

 

2.linux内核如何描述混杂设备

struct miscdevice {

       int minor;

       const char *name;

       const struct file_operations *fops;

};

minor:次设备号,用于区分各个混杂设备,如果让内核帮你分配一个次设备号,可以指定为MISC_DYNAMIC_MINOR

name:设备文件名,混杂设备文件名内核会帮你创建;

fops:给混杂设备提供的访问硬件的方法,并且将这些方法提供用户使用;

 

3.如何实现一个混杂设备驱动

分配初始化一个混杂设备对象:

struct file_operations led_fops = {

       ...

};

struct miscdevice led_misc = {

       .minor= MISC_DYNAMIC_MINOR, //动态分配

       .name= "myled" , //dev/myled

       .fops= &led_fops

};

注册混杂设备:misc_register(&led_misc);

卸载混杂设备:misc_deregister(&led_misic);

 

案例:利用混杂设备驱动来实现LED

GPIO操作方法:

1.利用GPIO库函数

2.ioremap寄存器,然后再操作寄存器

 

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

模拟信号:声音,电压,电流,温度,湿度,压力,速度;

数字信号:0,1这些数字量组成的信号

数字系统不能直接处理模拟信号,需要将模拟信号转换成对应的数字信号;

 

不管是数字信号还是模拟信号都是描述的同一个实物!

 

信号之间的转换:

AD:模拟信号转数字信号过程;

DA:数字信号转成模拟信号过程;

ADC:将模拟信号转数字信号的硬件单元;

DAC:将数字信号转模拟信号的硬件单元;

 

案例:手机的录音和放音

录音:就是将声音模拟信号转成数字信号的过程;手机必须有ADC硬件单元;

放音:就是将声音的数字信号转成模拟信号的过程;手机必须有DAC硬件单元;

手机的ADC和DAC都是集成在声卡芯片(AUDIO CODEC)中.

 

S5PV210自带ADC的硬件特性,芯片手册1978页:

模拟输入通道有10路:AIN0~AIN9,同一时刻只能转换一路模拟信号;这10个IO不能复用,只能做INPUT;

工作频率:最大是5MHz,时钟源是PCLK=66MHz,注意需要做降频!

ADC转换器一旦开启信号的转换,转换过程需要时间,一旦转换结束,ADC硬件给CPU产生一个中断信号,通知CPU转换结束

衡量ADC的工作参数指标:分辨率,自带ADC的分辨率为10位或者12位;

如果分辨率采用10位,就是代表将模拟信号转成数字信号以后,有效的数字量的位数为10位;

如果分辨率采用12,就是代表将模拟信号转成数字信号以后,有效的数字量的位数为12位;

 

模拟输入电压的范围为0~3.3V

注意:最大的模拟输入电压为3.3V,

如果分辨率为10位,那么每一个bit位对应的模拟电压是:

3.3/(1 << 10) = 3.22mV

如果分辨率为12,那么每一个bit位对应的模拟电压是:

3.3/(1 << 12) = 0.81mV

问:

如果现在已知一个10bit的转换以后的数字量为0011000000,请问这个数字量对应的模拟量值为多少?

模拟量的值 =0011000000 * 3.32mV

 

ADC操作涉及的寄存器:

寄存器组的基地址:0xE1700000

ADC控制寄存器:

bit16->配置分辨率,0:10位,1:12位

bit15->判断是否转换结束,0:转换进行中,1:转换结束

结论:判断ADC转换结束的方法:中断,轮询查询bit15

bit14->使能分频,0:不使能,1:使能

bit6-bit13:设置分频系数,如果ADC的工作频率为3.3MHZ,这个寄存器的值为66/3.3 - 1=19

bit0:启动ADC转换,1:表示启动ADC

 

ADC数据寄存器:保存ADC的转换结果

bit0~bit11:共12bit,范围0~0xFFF

注意普通的ADC转换的有效值:

分辨率为10位:data = 数据寄存器的值&0x3ff;

分辨率为12位: data = 数据寄存器的值&0xfff;

 

ADC中断清除寄存器:清ADC的中断,中断处理函数中清

bit0:写0或者1清中断

写任何值都可清除中断!

ADC模拟信号通过选择寄存器:选择模拟输入通道

ADC操作步骤:

1.设置正常的工作频率,3.3MHz

2.设置分辨率,12

3.设置模拟输入通道,AIN1

4.启动硬件ADC,硬件ADC开始对模拟电压信号进行转换

5.ADC转换结束产生中断信号

6.中断处理函数中清中断,唤醒休眠的进程

7.进程读取转换以后的数字量

 

软件实现:

1.用户需求

   在QT图形界面上,每隔5秒数动态刷新显示电压值

   在QT图形界面上,配置ADC的分辨率和模拟输入通道

2.驱动的设计:

1.对用户提供的接口:

   read:读转换以后的电压值;

       启动ADC

       判断是否转换结束,如果没有,进入休眠

       读取转换以后的数字量

       上报到用户空间

  ioctl:配置分辨率和模拟输入通道

2.采用混杂设备驱动实现方法+platform

3.寄存器地址要进行ioremap

4.注册中断处理函数:IRQ_ADC中断号

5.由于ADC的处理速度慢于CPU(应用程序的读取速度),所以在ADC没有转换结束时,进程进入休眠状态(等待队列机制)

6.在ADC操作之前,还要设置ADC的默认工作参数

 

 

 

 

 

 

 

 

 

 

 

 

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

余额充值