如何编写基于ARM的裸机程序和基于Linux的驱动程序

前言
在嵌入式开发中,ADC应用比较频繁,本文主要讲解ADC的基本原理以及如何编写基于ARM的裸机程序和基于Linux的驱动程序 。
ARM架构:Cortex-A9 Linux内核:3.14
在讲述ADC之前,我们需要先了解什么是模拟信号和数字信号 。
模拟信号
主要是与离散的数字信号相对的连续的信号 。模拟信号分布于自然界的各个角落,如每天温度的变化,而数字信号是人为的抽象出来的在时间上不连续的信号 。电学上的模拟信号是主要是指幅度和相位都连续的电信号,此信号可以被模拟电路进行各种运算,如放大,相加,相乘等 。
模拟信号是指用连续变化的物理量表示的信息,其信号的幅度,或频率,或相位随时间作连续变化,如目前广播的声音信号,或图像信号等 。
如下图所示从上到下一次是正弦波、 调幅波、 阻尼震荡波、 指数衰减波。

如何编写基于ARM的裸机程序和基于Linux的驱动程序uart.h" unsigned char table[10] = {'0','1','2','3','4','5','6','7','8','9'}; void mydelay_ms(int time) {int i, j;while(time--){for (i = 0; i < 5; i++)for (j = 0; j < 514; j++);} } adc_init(int temp) {ADCCON = (1 << 16 | 1 << 14 | 99 <<6 | 1 << 1);ADCMUX = 3;temp = ADCDAT & 0xfff; } /**裸机代码,不同于LINUX 应用层,一定加循环控制*/ int main (void) {unsigned char bit4,bit3,bit2,bit1;unsigned int temp = 0;uart_init();adc_init(temp);puts("开始转换 ");while(1){while(!(ADCCON & 0x8000));temp = ADCDAT & 0xfff;printf("U = %d ",temp);temp = 1.8 * 1000 * temp/0xfff;bit4 = temp /1000;putc(table[bit4]);bit3 = (temp % 1000)/100?;putc(table[bit3]);bit2 = ((temp % 1000)%100)/10;putc(table[bit2]);bit1 = ((temp % 1000)%100)%10;putc(table[bit1]);puts("mV");putc(' ');mydelay_ms(1000);}return 0; }
中断模式
中断模式读取数据步骤如下:
1.要读取数据首先向ADC寄存器ADCCON的bit:0写1,发送转换命令;
2.当ADC控制器转换完毕会通过中断线向CPU发送中断信号;
3.在中断处理函数中,读走数据,并清中断.
注:中断对应寄存器的设置,后续会更新对应的文档 。
void do_irq(void) {int irq_num;irq_num = CPU0.ICCIAR &0x3ff;switch(irq_num){case 42:adc_num = ADCDAT&0xfff;printf("adc = %d ",adc_num);CLRINTADC = 0;//IECR2 = IECR2 | (1 << 19);打开的话只能读取一次,//42/32ICDICPR.ICDICPR1 = ICDICPR.ICDICPR1 | (1 << 10);【清GIC中断标志位类似于 ICDISER】break;}CPU0.ICCEOIR = CPU0.ICCEOIR & (~0x3ff) | irq_num; } void adc_init(void) {//12bit使能分频分频值手动ADCCON = (1 << 16) | (1 << 14) | (0xff << 6) | (1 << 0);ADCMUX = 3; } void adcint_init(void) {IESR2 = IESR2 | (1 << 19);ICDDCR = 1;//使能分配器//42/32ICDISER.ICDISER1 = ICDISER.ICDISER1 | (1 << 10);//使能相应中断到分配器ICDIPTR.ICDIPTR10 = ICDIPTR.ICDIPTR10 &(~(0xff << 16)) | (0x1 << 16);//发送到相应CPU接口CPU0.ICCPMR = 255;//设置中断屏蔽优先级CPU0.ICCICR = 1;//全局使能开关 } int main (void) {adc_init();adcint_init();while(1){ADCCON = ADCCON | 1;delay_ms(1000);}return 0; }
基于Linux驱动编写
设备树
编写基于Linux的ADC外设驱动,首先需要编写设备树节点信息,在裸机程序中,我们只用到了寄存器地址,而编写基于Linux的驱动,我们需要用到中断功能 。所以编写设备树节点需要知道ADC要用到的硬件资源主要包括:寄存器资源和中断资源 。
关于中断的使用我们在后续文章中会继续分析,现在我们只需要知道中断信息如何填写即可 。
ADC寄存器信息填写

如何编写基于ARM的裸机程序和基于Linux的驱动程序

文章插图
由上可知,寄存器基地址为0x126c0000,其他寄存器只需要根据基地址做偏移即可获取,所以设备树的reg属性信息如下:
reg = <0x126C0000 0x20>;
ADC中断信息填写
描述中断连接需要四个属性:
父节点提供以下信息
interrupt-controller - 一个空的属性定义该节点作为一个接收中断信号的设备 。interrupt-cells- 这是一个中断控制器节点的属性 。它声明了该中断控制器的 中断指示符中【interrupts】 cell 的个数(类似于 #address-cells 和 #size-cells) 。
子节点描述信息
interrupt-parent - 这是一个设备节点的属性,包含一个指向该设备连接的中断控制器的 phandle 。那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性 。iterrupts- 一个设备节点属性,包含一个中断指示符的列表,对应于该设备上的 每个中断输出信号 。【设备的中断信息放在该属性中】
父节点
首先我们必须知道ADC控制器的中断线的父节点:

如何编写基于ARM的裸机程序和基于Linux的驱动程序
文章插图
由上图可知ADC控制器位于soc内,4个ADC通道公用一根中断线,该中断线连接在combiner上,所以我们需要查找到combiner这个父节点的说明:
进入设备树文件所在目录:archarmootdts
grep combiner *.* -n
经过筛选得到以下信息:

如何编写基于ARM的裸机程序和基于Linux的驱动程序
文章插图
因为我们使用的板子是exynos4412,而exynos系列通用的平台设备树文件是exynos4.dtsi,查看该文件:

如何编写基于ARM的裸机程序和基于Linux的驱动程序
文章插图
上图列举了combiner控制器的详细信息:
interrupt-cells ;interrupt-cells =<2>;
所以ADC控制器中断控制器的interrupts属性应该有两个cell 。
interrupts属性填写
而设备的中断信息填写方式由内核的以下文档提供:
Documentationdevicetreeindingsinterrupt-controllerinterrupts.txt69. b) two cells70.------------71.The #interrupt-cells property is set to 2 and the first cell 72. defines the73.index of the interrupt within the controller, while the second cell is used74.to specify any of the following flags:75.- bits[3:0] trigger type and level flags76.1 = low-to-high edge triggered77.2 = high-to-low edge triggered78.4 = active high level-sensitive79.8 = active low level-sensitive
由以上信息可知,中断的第一个cell是该中断源所在中断控制器的index,第二个cell表示中断的触发方式
上升沿触发
下降沿触发
高电平触发
低电平触发
那么index应该是多少呢?
详见datasheet的9.2.2 GIC Interrupt Table 节:

如何编写基于ARM的裸机程序和基于Linux的驱动程序
文章插图
此处我们应该是填写左侧的SPI ID:10 还是填写INTERRUPT ID:42呢?
此处我们可以参考LCD节点的interrupts填写方法:
通过查找父节点为combiner的设备信息 。
继续grep combiner . -n

如何编写基于ARM的裸机程序和基于Linux的驱动程序
文章插图
由此可见lcd这个设备的interrupts属性index值是11,所以可知ADC控制器中断线的index是10 。中断信息如下:
interrupt-parent = <&combiner>; interrupts = <10 3>;
ADC外设设备树信息
fs4412-adc{compatible = "fs4412,adc";reg = <0x126C0000 0x20>;interrupt-parent = <&combiner>;interrupts = <10 3>; };
本文默认大家会使用设备树,不知道如何使用设备树的朋友,后续会开一篇单独讲解设备树 。
【注意】在不支持设备树内核中,以Cortex-A8为例,中断信息填写在以下文件中
内部中断,Irqs.h (archarmmach-s5pc100includemach) 外部中断在Irqs.h (archarmplat-s5pincludeplat)
ADC属于内部中断,位于archarmmach-s5pc100includemachIrqs.h中 。
寄存器信息填写在以下位置:
archarmmach-s5pc100Mach-smdkc100.cstatic struct platform_device *smdkc100_devices[] __initdata = http://www.dg8.com.cn/news/{&s3c_device_adc,&s3c_device_cfcon,&s3c_device_i2c0,&s3c_device_i2c1,&s3c_device_fb,&s3c_device_hsmmc0,&s3c_device_hsmmc1,&s3c_device_hsmmc2,&samsung_device_pwm,&s3c_device_ts,&s3c_device_wdt,&smdkc100_lcd_powerdev,&s5pc100_device_iis0,&samsung_device_keypad,&s5pc100_device_ac97,&s3c_device_rtc,&s5p_device_fimc0,&s5p_device_fimc1,&s5p_device_fimc2,&s5pc100_device_spdif, };
结构体s3c_device_adc定义在以下文件:
archarmplat-samsungDevs.c#ifdef CONFIG_PLAT_S3C24XX static struct resource s3c_adc_resource[] = {[0] = DEFINE_RES_MEM(S3C24XX_PA_ADC, S3C24XX_SZ_ADC),[1] = DEFINE_RES_IRQ(IRQ_TC),[2] = DEFINE_RES_IRQ(IRQ_ADC), }; struct platform_device s3c_device_adc = {.name= "s3c24xx-adc",.id= -1,.num_resources= ARRAY_SIZE(s3c_adc_resource),.resource= s3c_adc_resource, }; #endif /* CONFIG_PLAT_S3C24XX */ #if defined(CONFIG_SAMSUNG_DEV_ADC) static struct resource s3c_adc_resource[] = {[0] = DEFINE_RES_MEM(SAMSUNG_PA_ADC, SZ_256),[1] = DEFINE_RES_IRQ(IRQ_TC),[2] = DEFINE_RES_IRQ(IRQ_ADC), }; struct platform_device s3c_device_adc = {.name= "samsung-adc",.id= -1,.num_resources= ARRAY_SIZE(s3c_adc_resource),.resource= s3c_adc_resource, }; #endif /* CONFIG_SAMSUNG_DEV_ADC */
由代码可知,平台驱动对应的platform_device具体内容由宏CONFIG_PLAT_S3C24XX、CONFIG_SAMSUNG_DEV_ADC来控制 。
驱动编写架构和流程如下
read() {1、向adc设备发送要读取的命令ADCCON1<<0 | 1<<14 | 0X1<<16 | 0XFF<<62、读取不到数据就休眠wait_event_interruptible();3、等待被唤醒读数据havedata = http://www.dg8.com.cn/news/0; } adc_handler() {1、清中断 ADC使用中断来通知转换数据完毕的2、状态位置位;havedata=1;3、唤醒阻塞进程wake_up() } probe() {1、读取中断号,注册中断处理函数2、读取寄存器的地址,ioremap3、字符设备的操作 }
驱动需要首先捕获中断信号后再去寄存器读取相应的数据,在ADC控制器没有准备好数据之前,应用层需要阻塞读取数据,所以在读取数据的函数中,需要借助等待队列来实现驱动对应用进程的阻塞 。驱动程序
驱动程序对寄存器的操作参考裸机程序,只是基地址需要通过ioremap()做映射,对寄存器的读写操作需要用readl、writel 。
driver.c
#include #include #include #include #include #include #include #include#includestatic int major = 250;static wait_queue_head_t wq; static int have_data = http://www.dg8.com.cn/news/0; static int adc; static struct resource *res1; static struct resource *res2; static void *adc_base;#define ADCCON 0x0000 #define ADCDLY 0x0008 #define ADCDAT 0x000C #define CLRINTADC 0x0018 #define ADCMUX 0x001Cstaticirqreturn_t adc_handler(int irqno, void *dev) {have_data = 1;printk("11111 ");/*清中断*/writel(0x12,adc_base + CLRINTADC);wake_up_interruptible(&wq);return IRQ_HANDLED; } static int adc_open (struct inode *inod, struct file *filep) {return 0; } static ssize_t adc_read(struct file *filep, char __user *buf, size_t len, loff_t *pos) {writel(0x3,adc_base + ADCMUX);writel(1<<0 | 1<<14 | 0X1<<16 | 0XFF<<6 ,adc_base +ADCCON );wait_event_interruptible(wq, have_data=http://www.dg8.com.cn/news/=1);/*read data*/adc = readl(adc_base+ADCDAT)&0xfff;if(copy_to_user(buf,&adc,sizeof(int))){return -EFAULT;}have_data = 0;return len; } staticint adc_release(struct inode *inode, struct file *filep) {return 0; } static struct file_operationsadc_ops = {.open = adc_open,.release = adc_release,.read = adc_read, };static int hello_probe(struct platform_device *pdev) {int ret;printk("match 0k");res1 = platform_get_resource(pdev,IORESOURCE_IRQ, 0);res2 = platform_get_resource(pdev,IORESOURCE_MEM, 0);ret = request_irq(res1->start,adc_handler,IRQF_DISABLED,"adc1",NULL);adc_base = ioremap(res2->start,res2->end-res2->start);register_chrdev( major, "adc", &adc_ops);init_waitqueue_head(&wq);return 0; } static int hello_remove(struct platform_device *pdev) {free_irq(res1->start,NULL);free_irq(res2->start,NULL);unregister_chrdev( major, "adc");return 0; }static struct of_device_id adc_id[]= {{.compatible = "fs4412,adc" }, };static struct platform_driver hello_driver= {.probe = hello_probe,.remove = hello_remove,.driver ={.name = "bigbang",.of_match_table = adc_id,}, };static int hello_init(void) {printk("hello_init");return platform_driver_register(&hello_driver); } static void hello_exit(void) {platform_driver_unregister(&hello_driver);printk("hello_exit");return; } MODULE_LICENSE("GPL"); module_init(hello_init); module_exit(hello_exit);
测试程序
test.c
【如何编写基于ARM的裸机程序和基于Linux的驱动程序】 #include#include#include #includemain() {int fd,len;int adc;fd = open("/dev/hello",O_RDWR);if(fd<0){perror("open fail");return ;}while(1){read(fd,&adc,4);printf("adc%0.2f V",(1.8*adc)/4096);}close(fd); }

    推荐阅读