拨开迷雾-单片机和嵌入式LINUX开发的那点事儿

作者:韦东山,华清远见嵌入式学院讲师。

随着嵌入式行业硬件平台的性能增强,项目需求和功能日益复杂,ARM公司推出的 CORTEX-M3,更是让以往做单片机的工程师在芯片和技术选型面临两难选择,本专题将从芯片价格、整个系统的硬件软件设计及维护的成本等各个方面给您提供一个参考, 并从技术角度分析单片机和带操作系统的系统的软件开发的异同点。

●    1.单片机与ARM等新处理器的价格比较
        ●    2.带操作系统与不带操作系统的软件开发的区别
        ●    2.1.驱动开发的区别
        ●    2.2.应用程序开发的区别

1. 单片机与ARM等新处理器的价格比较

表1

型号

架构

资源

价格(元)

AT89S51

8051

最高频率33MHz
4KB Flash
128B内部RAM
32个可编程IO引脚
两个16bit的计数器
一个UART口

4

SST89E564RD

8051

最高频率40MHz
64KB Flash
1KB内部RAM
32个可编程IO引脚
三个16bit的计数器
一个UART口
一个SPI接口

35

STM32F103

CORTEX-M3

最高频率72MHz
64KB或128BK Flash
20KB SRAM
80个可编程IO引脚
2个12bit的ADC
7通道DMA控制器
标准调试口(SWD和JTAG)
7个定时器
USB接口
2个I2C接口
3个UART
2个SPI接口

21

HI3510

ARM9+DSP双核

最高工作频率240MHz
视频处理单元,支持多种协议的实时编解码
图形处理单元
视音频接口
以太网接口
DDR控制器
USB、UART、IrDA、
I2C、SPI、GPIO等多种外设接口

80

S3C2440

ARM9

最高工作频率400MHz
SDRAM控制器
LCD控制器
4通道DMA控制器
3个UART
2个SPI
1个I2C接口
IIS音频接口
SD HOST接口
2个USB接口
8个10bit ADC
摄像头接口
Camera接口

40

 

从表1里面各种芯片的资源,大概就可以猜知它们的应用场合。51单片机通常被用来做一些比较简单的控制,比如采集信号、驱动一些开关。AT89S51的Flash只有4K,一个稍微复杂的程序就不止4K了。SST89E564RD是一种扩展的51单片机,它的Flash达到64KB,可以外接最多64KB的SRAM。在SST89E564RD上的程序可以写得更复杂一些,但是它对外的接口也比较少。

CORTEX-M3系列的处理器,对外接口极其丰富,这使得它的应用面更广,但是限于它的Flash、内存还是比较小,一般不在上面运行操作系统,它算是一个性能非常突出的单片机。

HI3510是海思半导体公司的一款用于监控设备的芯片,一般上面运行Linux系统,通过摄像头采集数据、编码,然后通过网络传输。另一端接收到数据之后,再解码。在上面运行的程序非常复杂,有漂亮的图片界面、触摸屏控制、数据库等等。对声音图像的编解码更是用到DSP核。

S3C2440是一款通用的芯片,它与“高级单片机”STM32F103相比,多了存储控制器和NAND控制器──这使得可以外接更大的Flash、更大的内存;多了内存管理单元(MMU)──这使得它可以进行地址映身(虚拟地址、物理地址之间的映射)。可以在S3C2440上运行Linux系统,运行更大更复杂的程序。

在具体工作中,怎么选择这些芯片呢?一句话:成本!进行任何产品的开发都要考虑性价比,一切应该从“成本”出发。成本不仅包括芯片的价格,也包括整个系统的硬件、软件设计及维护的难易。

芯片价格可以在电子市场问到,也可以在http://www.ic.net.cn上找到有卖这种芯片的柜台,然后电话咨询。

基于不同的应用,处理器和其他外设的选择是要统一考虑的,如果要实现一个简单的U盘读写功能,那么可以选择带USB控制器的CORTEX-M3芯片,也可以选择8051外接一个USB控制器比如SL811,就看哪种方案成本更低。进行芯片选型时,必须基于整个系统来考虑。

员工的偏好和知识结构也是一个很重要的因素,如果他对ATMEL的芯片比较熟,他就不会倾向于三星;如果他不会Linux等操作系统,那么选型时就不会有操作系统的概念。选择自己不熟悉的芯片和技术,最后的成本也可能更高。

2. 带操作系统与不带操作系统的软件开发的区别

用通俗的话来说,一个处理芯片不运行操作系统,我们就把它称为单片机,而单片机编程就是写裸板程序,这个程序直接在板子上运行;相对的,另一种程序就是基于操作系统的程序,说得简单点就是,这种程序可以通过统一的接口调用“别人写好的代码”,在“别人的基础上”更快更方便地实现自己的功能。

2.1. 驱动开发的区别

驱动开发的区别我总结有两点:能否借用、是否通用。

2.1.1 能否借用

基于操作系统的软件资源非常丰富,你要写一个Linux设备驱动时,首先在网上找找,如果有直接拿来用;其次是找到类似的,在它的基础上进行修改;如果实在没有,就要研究设备手册,从零写起。而不带操作系统的驱动开发,一开始就要深入了解设备手册,从零开始为它构造运行环境,实现各种函数以供应用程序使用。

举个例子,要驱动一块LCD,在单片机上的做法是:

① 首先要了解LCD的规格,弄清楚怎么设置各个寄存器,比如设置LCD的时钟、分辨率、象素
② 划出一块内存给LCD使用
③ 编写一个函数,实现在指定坐标描点。比如根据x、y坐标在这块内存里找到这个象素对应的小区域,填入数据。

基于操作系统时,我们首先是找到类似的驱动,弄清楚驱动结构,找到要修改的地方进行修改。

下面是单片机操作LCD的代码:

① 初始化:

void Tft_Lcd_Init(int type)
        {
               /*
                * 设置LCD控制器的控制寄存器LCDCON1~5
                * 1. LCDCON1:
                * 设置VCLK的频率:VCLK(Hz) = HCLK/[(CLKVAL+1)x2]
                * 选择LCD类型: TFT LCD
                * 设置显示模式: 16BPP
                * 先禁止LCD信号输出
                * 2. LCDCON2/3/4:
                * 设置控制信号的时间参数
                * 设置分辨率,即行数及列数
                * 现在,可以根据公式计算出显示器的频率:
                * 当HCLK=100MHz时,
                * Frame Rate = 1/[{(VSPW+1)+(VBPD+1)+(LIINEVAL+1)+(VFPD+1)}x
                *        {(HSPW+1)+(HBPD+1)+(HFPD+1)+(HOZVAL+1)}x
                *        {2x(CLKVAL+1)/(HCLK)}]
                *        = 60Hz
                * 3. LCDCON5:
                *        设置显示模式为16BPP时的数据格式: 5:6:5
                *        设置HSYNC、VSYNC脉冲的极性(这需要参考具体LCD的接口信号): 反转
                *        半字(2字节)交换使能
                */
                LCDCON1 = (CLKVAL_TFT_320240<<8) | (LCDTYPE_TFT<<5) | /
                                        (BPPMODE_16BPP<<1) | (ENVID_DISABLE<<0);
                LCDCON2 = (VBPD_320240<<24) | (LINEVAL_TFT_320240<<14) | /
                                        (VFPD_320240<<6) | (VSPW_320240);
                LCDCON3 = (HBPD_320240<<19) | (HOZVAL_TFT_320240<<8) | (HFPD_320240);
                LCDCON4 = HSPW_320240;
        //        LCDCON5 = (FORMAT8BPP_565<<11) | (HSYNC_INV<<9) | (VSYNC_INV<<8) | /
        //                                (HWSWP<<1);
                        LCDCON5 = (FORMAT8BPP_565<<11) | (HSYNC_INV<<9) | (VSYNC_INV<<8) | (VDEN_INV << 6) | /
                                                (HWSWP<<0);

        /*
                * 设置LCD控制器的地址寄存器LCDSADDR1~3
                * 帧内存与视口(view point)完全吻合,
                * 图像数据格式如下:
                *        |----PAGEWIDTH----|
                *        y/x    0    1    2    239
                *        0    rgb rgb rgb ... rgb
                *        1    rgb rgb rgb ... rgb
                * 1. LCDSADDR1:
                *    设置LCDBANK、LCDBASEU
                * 2. LCDSADDR2:
                *    设置LCDBASEL: 帧缓冲区的结束地址A[21:1]
                * 3. LCDSADDR3:
                *    OFFSIZE等于0,PAGEWIDTH等于(240*2/2)
                */
                LCDSADDR1 = ((LCDFRAMEBUFFER>>22)<<21) | LOWER21BITS(LCDFRAMEBUFFER>>1);
                LCDSADDR2 = LOWER21BITS((LCDFRAMEBUFFER+ /
                                            (LINEVAL_TFT_320240+1)*(HOZVAL_TFT_320240+1)*2)>>1);
                LCDSADDR3 = (0<<11) | (LCD_XSIZE_TFT_320240*2/2);

        /* 禁止临时调色板寄存器 */
                TPAL = 0;

        fb_base_addr = LCDFRAMEBUFFER;
                bpp = 16;
                xsize = 320;
                ysize = 240;
        }

② 描点:

/*
        * 画点
        * 输入参数:
        *        x、y : 象素坐标
        *        color: 颜色值
        *                对于16BPP: color的格式为0xAARRGGBB (AA = 透明度),
        *        需要转换为5:6:5格式
        *                对于8BPP: color为调色板中的索引值,
        *        其颜色取决于调色板中的数值
        */
        void PutPixel(UINT32 x, UINT32 y, UINT32 color)
        {
                UINT8 red,green,blue;

        switch (bpp){
                        case 16:
                        {
                                UINT16 *addr = (UINT16 *)fb_base_addr + (y * xsize + x);
                                red = (color >> 19) & 0x1f;
                                green = (color >> 10) & 0x3f;
                                blue = (color >> 3) & 0x1f;
                                color = (red << 11) | (green << 5) | blue; // 格式5:6:5
                                *addr = (UINT16) color;
                                break;
                        }

                        case 8:
                        {
                                UINT8 *addr = (UINT8 *)fb_base_addr + (y * xsize + x);
                                *addr = (UINT8) color;
                                break;
                        }

                default:
                                break;
                }
        }

下面是在Linux的LCD驱动里修改的地方(arch/arm/mach-s3c2440/mach-smdk2440.c):
        /* 320x240 */
        static struct s3c2410fb_mach_info smdk2440_lcd_cfg __initdata = {
                .regs = {
                        .lcdcon1 = S3C2410_LCDCON1_TFT16BPP | /
                                S3C2410_LCDCON1_TFT | /
                                S3C2410_LCDCON1_CLKVAL(0x04),

                .lcdcon2 = S3C2410_LCDCON2_VBPD(1) | /
                                S3C2410_LCDCON2_LINEVAL(239) | /
                                S3C2410_LCDCON2_VFPD(5) | /
                                S3C2410_LCDCON2_VSPW(1),

                .lcdcon3 = S3C2410_LCDCON3_HBPD(36) | /
                                S3C2410_LCDCON3_HOZVAL(319) | /
                                S3C2410_LCDCON3_HFPD(19),

                .lcdcon4 = S3C2410_LCDCON4_MVAL(13) | /
                                S3C2410_LCDCON4_HSPW(5),

                .lcdcon5 = S3C2410_LCDCON5_FRM565 |
                                S3C2410_LCDCON5_INVVLINE |
                                S3C2410_LCDCON5_INVVFRAME |
                                S3C2410_LCDCON5_INVVDEN |
                                S3C2410_LCDCON5_PWREN |
                                S3C2410_LCDCON5_HWSWP,
                },

        .gpccon = 0xaaaa56aa,
                .gpccon_mask = 0xffffffff,
                .gpcup = 0xffffffff,
                .gpcup_mask = 0xffffffff,

        .gpdcon = 0xaaaaaaaa,
                .gpdcon_mask = 0xffffffff,
                .gpdup = 0xffffffff,
                .gpdup_mask = 0xffffffff,

        .fixed_syncs = 1,
                .type = S3C2410_LCDCON1_TFT,
                .width = 320,
                .height = 240,

        .xres = {
                        .min = 320,
                        .max = 320,
                        .defval = 320,
                },

        .yres = {
                        .max = 240,
                        .min = 240,
                        .defval = 240,
                },

        .bpp = {
                        .min = 16,
                        .max = 16,
                        .defval = 16,
                },
        };

这并不表示代码Linux的驱动程序就比单片机的驱动程序好写,怎么在几万个文件中找到要修改的代码,这也是需要艰苦的学习的。基于操作系统的驱动开发,既要懂得芯片的具体操作,也要理解操作系统的软件结构。

2.1.2 是否通用

有些单片机厂家也给客户提供了大量的驱动程序,比如USB HOST驱动程序,这可以让客户很容易就可以在它的上面编写程序读写U盘。但是客户写的这些程序,只能在这种芯片、这个驱动程序上使用;更换另一种芯片后,即使芯片公司也提供了驱动程序,但是接口绝对不一样,客户又得重新编写应用程序。
基于操作系统的驱动程序要遵循统一的接口,比如对于不同的芯片的USB HOST驱动,它们都要向上提供一个相同的数据结构,在里面实现了各自的USB操作。

下面是S3C2410/S3C2440的USB驱动向上层提供的数据结构:

static const struct hc_driver ohci_s3c2410_hc_driver = {
                .description = hcd_name,
                .product_desc = "S3C24XX OHCI",
                .hcd_priv_size = sizeof(struct ohci_hcd),

        /*
                * generic hardware linkage
                */
                .irq = ohci_irq,
                .flags = HCD_USB11 | HCD_MEMORY,

        /*
                * basic lifecycle operations
                */
                .start = ohci_s3c2410_start,
                .stop = ohci_stop,
                .shutdown = ohci_shutdown,

        /*
                * managing i/o requests and associated device resources
                */
                .urb_enqueue = ohci_urb_enqueue,
                .urb_dequeue = ohci_urb_dequeue,
                .endpoint_disable = ohci_endpoint_disable,

        /*
                * scheduling support
                */
                .get_frame_number = ohci_get_frame,

        /*
                * root hub support
                */
                .hub_status_data = ohci_s3c2410_hub_status_data,
                .hub_control = ohci_s3c2410_hub_control,
                .hub_irq_enable = ohci_rhsc_enable,
        #ifdef CONFIG_PM
                .bus_suspend = ohci_bus_suspend,
                .bus_resume = ohci_bus_resume,
        #endif
                .start_port_reset = ohci_start_port_reset,
        };

下面是ATMEL公司的ARM芯片的USB驱动向上层提供的数据结构:

/*-------------------------------------------------------------------------*/

static const struct hc_driver ohci_at91_hc_driver = {
                .description = hcd_name,
                .product_desc = "AT91 OHCI",
                .hcd_priv_size = sizeof(struct ohci_hcd),

        /*
                * generic hardware linkage
                */
                .irq = ohci_irq,
                .flags = HCD_USB11 | HCD_MEMORY,

        /*
                * basic lifecycle operations
                */
                .start = ohci_at91_start,
                .stop = ohci_stop,
                .shutdown = ohci_shutdown,

        /*
                * managing i/o requests and associated device resources
                */
                .urb_enqueue = ohci_urb_enqueue,
                .urb_dequeue = ohci_urb_dequeue,
                .endpoint_disable = ohci_endpoint_disable,

        /*
                * scheduling support
                */
                .get_frame_number = ohci_get_frame,

        /*
                * root hub support
                */
                .hub_status_data = ohci_hub_status_data,
                .hub_control = ohci_hub_control,
                .hub_irq_enable = ohci_rhsc_enable,
        #ifdef CONFIG_PM
                .bus_suspend = ohci_bus_suspend,
                .bus_resume = ohci_bus_resume,
        #endif
                .start_port_reset = ohci_start_port_reset,
        };

基于通用性,即使是你自己写的Linux驱动,简单到只是点亮一个LED,基于“通用性”,这个驱动也要向上提供统一的接口。下面是单片机LED驱动程序和Linux下的LED驱动程序的部分代码。

单片机LED驱动程序:

void led_init(void)
        {
                GPBCON = GPB5_out; // 将LED对应的GPB5引脚设为输出
        }

void led_on(void)
        {
                GPBDAT &= ~(1<<5);
        }

void led_off(void)
        {
                GPBDAT |= (1<<5);
        }

Linux的LED驱动程序:
        #define DEVICE_NAME "leds" /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
        #define LED_MAJOR 231 /* 主设备号 */

/* 应用程序执行ioctl(fd, cmd, arg)时的第2个参数 */
        #define IOCTL_LED_ON 0
        #define IOCTL_LED_OFF 1

/* 用来指定LED所用的GPIO引脚 */
        static unsigned long led_table [] = {
                S3C2410_GPB5,
                S3C2410_GPB6,
                S3C2410_GPB7,
                S3C2410_GPB8,
        };

/* 用来指定GPIO引脚的功能:输出 */
        static unsigned int led_cfg_table [] = {
                S3C2410_GPB5_OUTP,
                S3C2410_GPB6_OUTP,
                S3C2410_GPB7_OUTP,
                S3C2410_GPB8_OUTP,
        };

/* 应用程序对设备文件/dev/leds执行open(...)时,
        * 就会调用s3c24xx_leds_open函数
        */
        static int s3c24xx_leds_open(struct inode *inode, struct file *file)
        {
                int i;

                for (i = 0; i < 4; i++) {
                        // 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能
                        s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);
                }
                return 0;
        }

/* 应用程序对设备文件/dev/leds执行ioclt(...)时,
        * 就会调用s3c24xx_leds_ioctl函数
        */
        static int s3c24xx_leds_ioctl(
                struct inode *inode,
                struct file *file,
                unsigned int cmd,
                unsigned long arg)
        {
                if (arg > 4) {
                        return -EINVAL;
                }

                switch(cmd) {
                case IOCTL_LED_ON:
                        // 设置指定引脚的输出电平为0
                        s3c2410_gpio_setpin(led_table[arg], 0);
                        return 0;

        case IOCTL_LED_OFF:
                        // 设置指定引脚的输出电平为1
                        s3c2410_gpio_setpin(led_table[arg], 1);
                        return 0;

        default:
                        return -EINVAL;
                }
        }

/* 这个结构是字符设备驱动程序的核心
        * 当应用程序操作设备文件时所调用的open、read、write等函数,
        * 最终会调用这个结构中指定的对应函数
        */
        static struct file_operations s3c24xx_leds_fops = {
                .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
                .open = s3c24xx_leds_open,
                .ioctl = s3c24xx_leds_ioctl,
        };

/*
        * 执行insmod命令时就会调用这个函数
        */
        static int __init s3c24xx_leds_init(void)
        {
                int ret;

        /* 注册字符设备
                * 参数为主设备号、设备名字、file_operations结构;
                * 这样,主设备号就和具体的file_operations结构联系起来了,
                * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
                * LED_MAJOR可以设为0,表示由内核自动分配主设备号
                */
                ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);
                if (ret < 0) {
                        printk(DEVICE_NAME " can't register major number/n");
                        return ret;
                }

                printk(DEVICE_NAME " initialized/n");
                return 0;
        }

/*
        * 执行rmmod命令时就会调用这个函数
        */
        static void __exit s3c24xx_leds_exit(void)
        {
                /* 卸载驱动程序 */
                unregister_chrdev(LED_MAJOR, DEVICE_NAME);
        }

/* 这两行指定驱动程序的初始化函数和卸载函数 */
        module_init(s3c24xx_leds_init);
        module_exit(s3c24xx_leds_exit);

2.2. 应用程序开发的区别

2.2.1 对于不带操作系统的应用编程,应用程序和驱动程序之间的间隔并不明显。

举个例子,要在LCD上显示字母“a”,在单片机上的做法是:
        ① 事先在Flash上保存“a”的点阵数据,假设它的象素大小是8x8,那么这个点阵大小就是8x8=64 bits,即8字节
        ② 应用程序读取这64bit数据,逐个象素地在LCD上描点

相对的,基于操作系统的应用编程,就不需要懂得硬件知识,执行一个简单的“echo a > /dev/tty1”就可以在LCD上显示“a”了。

2.2.2 不带操作系统的应用程序,可借用的软件资源很少;

带操作系统的应用程序,网上各种开源的软件很多。

比如要做一个播放器,在不带操作系统上实现会非常困难;如果是在Linux下,有现成的。

2.2.3 不带操作系统的应用程序,各个任务是串行执行的;

带操作系统的应用程序,各个任务是并行执行的。

2.2.4 不带操作系统的应用程序,一旦发生程序错误,整个系统将崩溃

带操作系统的应用程序,即使发生了程序错误,操作系统本身并不会崩溃

你可能感兴趣的:(linux嵌入式方向)