uc-PC板子上跑的是Linux 4.13.3,使用设备树。内核不经压缩,就地运行(XIP,全称execute in place)。

需要使能的功能有:网络、显示、USB、SD卡、摄像头、yaffs。这些功能全部开启的话,内核大小将为4MB以上,而片内flash只有2MB,不模块化的话只能跑在SDRAM上,这回导致性能低下,所以调通设备驱动之后应该进行模块化,将关键部分编译进内核,内核塞进片内flash,从而提高性能。

以下将只描述它们如何配置,而不涉及具体驱动的软件架构。

  • 配置之前,应该设置环境变量:ARCH=arm CROSS_COMPILE=arm-none-eabi-(其他编译工具链设置也类似),不然的话会按照host的架构去配置。
  • 配置时一般用make menuconfig,如果装了qt的话还可以用make xconfig;后者可以用鼠标操作,全屏的话看到的信息量更多。
  • 搜索某一个配置时,不用加CONFIG_前缀,而且不用管大小写。
  • 某一个配置没有显示出来,是因为它依赖的配置还没有使能;这时就要耐着性子去一个个翻找。。。

(手机请横屏看代码)

网络

Linux内核实现了相当完整而庞大的网络协议栈——对于单片机来说实在过于庞大;并且,协议栈部分是必须编译进内核而不能模块化的,因此这一块只能放置于片内flash中;而且,只在defconfig的基础上使能网络的话,内核就超过了2MB了。因此在调通板子上的驱动之前,内核都只能在SDRAM上面就地运行。。参考前文bootloader的配置。

下面实现基本的网络功能:获取IP、跑通nfs。诸如DNS之类的暂时不弄。

上位机搭建NFS服务器

$ sudo apt install nfs-common

我的nfs目录是/home/hyq/nfs,为此修改配置文件/etc/exports

/home/hyq/nfs *(rw,sync,no_root_squash,no_subtree_check)

重启服务以生效:

$ sudo /etc/init.d/nfs-kernel-server restart
一件小事:

有时候调板子是这种情况:电脑要连校园网WiFi来科学上网(因为有ipv6),板子连着一个不上网的路由器,电脑又需要通过网线来连那个路由器来访问板子。这时候电脑如果不插网线就能上网,插了就上不了网了。这是因为默认的路由指向了以太网。需要通过route命令来将路由改到WiFi上。首先查看路由表:

$ route
内核 IP 路由表
目标       网关         子网掩码   标志 跃点  引用 使用 接口
default   OpenWrt.lan  0.0.0.0   UG  100   0    0   enp2s0
...

我电脑的以太网接口是enp2s0,WiFi接口是wlp1s0(别的电脑可能分别是eth0和wlan0)。default到了enp2s0,将其路由到wlp1s0:

$ sudo route del default
$ sudo route add default gw <只连WiFi时候的网关> wlp1s0

此时默认就路由到WiFi上了。此时或许还可能上不了网但是能ping外网。此时就要在/etc/resolv.conf里面改DNS,比方说改为114.114.114.114,然后就可以上网了。

内核配置

在第一层次的配置中使能Networking support,然后在其下Networking options下面使能TCP/IP networking,我只留了这些配置:

最小网络配置
最小网络配置

并且添加网卡驱动:分别需要MAC和phy的驱动。在Device Drivers->Network device support->Ethernet driver support下面选择STM32的MAC。

MAC配置
MAC配置

phy的驱动可以选择默认的CONFIG_FIXED_PHY,也可以选择SMSC的phy,都可以成功驱动LAN8720A。在Device Drivers->Network device support->PHY Device support and infrastructure下选择。

网卡配置
网卡配置

然后修改设备树:
首先在stm32f429.dtsi里的pinctrl节点下增加一个RMII引脚的配置,并且在ethernet节点下面添加mac地址;mac地址可以随便设置,只要不跟局域网里面的设备重合就行了:

/ {
    ...
    soc {
        ...
        pinctrl: pin-controller {
            ...

            // RMII引脚复用的配置
            ethernet_rmii: rmii@0 {
                pins {
                    pinmux = <STM32F429_PB12_FUNC_ETH_MII_TXD0_ETH_RMII_TXD0>,
                        <STM32F429_PB13_FUNC_ETH_MII_TXD1_ETH_RMII_TXD1>,
                        <STM32F429_PB11_FUNC_ETH_MII_TX_EN_ETH_RMII_TX_EN>,
                        ...
                    slew-rate = <3>;
                };
            };
            ...
        };
        ...

        // ethernet节点
        mac: ethernet@40028000 {
            // 添加mac地址
            mac-address = [C0 B1 3D 88 88 89];
            // 其他属性照旧
        };
    };
};

然后在stm32f429-disco.dts里补充设置ethernet节点:

&mac {
    status = "okay";
    pinctrl-0   = <&ethernet_rmii>;
    pinctrl-names = "default";
    phy-mode    = "rmii";
    snps,reset-gpio = <&gpioh 2 GPIO_ACTIVE_HIGH>;
    snps,reset-active-low;
    snps,reset-delays-us = <0 10000 100000>;
};

这些设置的来源是Documentation/devicetree/bindings/net/stm32-dwmac.txt。这里并不需要像stm32429i-eval评估板设置的那么繁琐,只需要设置哪一套引脚、RMII模式,以及phy芯片的reset引脚即可。

源码里的小bug

stm32的MAC初始化是drivers/net/ethernet/stmicro/stmmac/dwmac-stm32.cstm32_dwmac_init(),它根据配置将外设设置为MII模式或RMII模式。原代码第42行左右RMII模式寄存器设置错误。本网卡只有RMII模式,而stm32429i-eval的网卡工作在MII模式,估计因此他们没查出这个bug。

-  val = (plat_dat->interface == PHY_INTERFACE_MODE_MII) ? 0 : 1;
+  val = (plat_dat->interface == PHY_INTERFACE_MODE_MII) ? 0 : MII_PHY_SEL_MASK;
启动的printk
[    3.430000] libphy: Fixed MDIO Bus: probed
[    3.470000] stm32-dwmac 40028000.ethernet: PTP uses main clock
[    3.480000] stm32-dwmac 40028000.ethernet: no reset control found
[    3.480000] stmmac - user ID: 0x10, Synopsys ID: 0x35
[    3.490000] stm32-dwmac 40028000.ethernet: Ring mode enabled
[    3.500000] stm32-dwmac 40028000.ethernet: DMA HW capability register supported
[    3.510000] stm32-dwmac 40028000.ethernet: Enhanced/Alternate descriptors
[    3.510000] stm32-dwmac 40028000.ethernet: Enabled extended descriptors
[    3.520000] stm32-dwmac 40028000.ethernet: RX Checksum Offload Engine supported
[    3.530000] stm32-dwmac 40028000.ethernet: COE Type 2
[    3.530000] stm32-dwmac 40028000.ethernet: TX Checksum insertion supported
[    3.540000] stm32-dwmac 40028000.ethernet: Wake-Up On Lan supported
[    3.550000] stm32-dwmac 40028000.ethernet: Enable RX Mitigation via HW Watchdog Timer
[    4.200000] libphy: stmmac: probed
...
[    4.330000] stm32_rtc 40002800.rtc: setting system clock to 2000-01-01 03:07:42 UTC (946696062)
[    4.540000] SMSC LAN8710/LAN8720 stmmac-0:00: attached PHY driver [SMSC LAN8710/LAN8720] (mii_bus:phy_addr=stmmac-0:00, irq=-1)
[    4.600000] stmmac_init_dma_engine, reset addr: 40028000
[    4.600000] stm32-dwmac 40028000.ethernet eth0: IEEE 1588-2008 Advanced Timestamp supported
[    6.930000] stm32-dwmac 40028000.ethernet eth0: Link is Up - 100Mbps/Full - flow control rx/tx

nfs作为根文件系统

首先电脑的nfs目录下面得有完整的rootfs;可以暂时先从网上下载一个可用的rootfs,然后解压到那里。

然后配置内核以使能NFS;并且需要配置ROOT_NFS:在File systems->Network File Systems下面即可配置。

nfs配置
nfs配置

最后在stm32f429-disco.dts里面添加内核参数。现在的风格是,kernel command line加在设备树的chosen节点下面,而不是uboot传tags。

/ {
    chosen {
        bootargs = "root=/dev/nfs rw "
          "ip=192.168.2.123:192.168.2.1:::stm32:eth0:on "
          "nfsroot=192.168.2.202:/home/hyq/nfs";
    };
    ...
};

其中,ip可以设置为ip=dhcp,也可以像上述那样设置一个固定的地址;nfsroot对应到上位机的ip地址和nfs目录。

ping

因为没有DNS,所以不能ping百度之类的域名,只能ping IP地址。而且,如果内核参数是ip=dhcp的话,可以ping通外网;如果只是一个固定地址就只能ping局域网里面的东西。。下面ping的是114.114.114.114这个DNS服务器:

ping外网IP地址
ping外网IP地址

显示

主流内核中STM32的LTDC驱动居然放在drivers/gpu/drm/stm/目录下面。。。想必ST认为它的DMA2D可以算是一个gpu了,于是就要用内核里面的DRM架构了。。实在志不在小。。而emcraft的uclinux中stm32显示驱动还仅仅放在drivers/video/目录下面。

对于单片机来说,使用DRM架构的一大缺点是它实在太庞大了,塞不进片内flash,只能模块化以跑在SDRAM里,因而性能低下;因此应该选用尽量简单的驱动以避免这一点。

使用内核中的DRM驱动

内核配置:开启DRM:

DRM
DRM

还要开启STM32的DRM支持:

STM32 DRM
STM32 DRM

还要开启一个panel。这里为简单起见,选择simple-panel

panel
panel

DRM架构需要有一个panel,即外接的那个液晶屏控制器。有些RGB的lcd也要用诸如I2C接口去配置一些奇怪的参数。板子上直接将RGB用DAC转化成VGA信号了,所以选择一个“哑”的panel,即CONFIG_PANEL_SIMPLE。在drivers/gpu/drm/panel/panel-simple.c里面实现了一大堆LCD,它们只需要开关背光灯就行了。于是在里面找一个想要的尺寸——比如标准VGA信号的640x480——的屏幕,这里选择了et057090dhu,然后稍微改一下那些时钟配置,适合60HZ的VGA:

static const struct drm_display_mode edt_et057090dhu_mode = {
    .clock = 31468,
    .hdisplay =    640,
    .hsync_start = 640 + 16,
    .hsync_end =   640 + 16 + 96,
    .htotal =      640 + 16 + 96 + 48,
    .vdisplay =    480,
    .vsync_start = 480 + 10,
    .vsync_end =   480 + 10 + 2,
    .vtotal =      480 + 10 + 2 + 33,
    .vrefresh = 60,
    .flags = DRM_MODE_FLAG_NVSYNC | DRM_MODE_FLAG_NHSYNC,
};

设备树需要增加panel-rgb节点,并且有两个endpoint相互指引。改写如下:

/{
    panel_rgb: panel-rgb {
        compatible = "edt,et057090dhu";
        status = "okay";
        port {
            panel_in_rgb: endpoint {
                // 链接到下面的ltdc_out_rgb节点
                remote-endpoint = <&ltdc_out_rgb>;
            };
        };
    };
    ...
};
&ltdc {
    status = "okay";
    pinctrl-0 = <&ltdc_pins>;
    pinctrl-names = "default";
    dma-ranges;
    port {
        ltdc_out_rgb: endpoint {
            // 链接到上面的panel_in_rgb节点
            remote-endpoint = <&panel_in_rgb>;
        };
    };
};

为了启用tty终端,还需要选择CONFIG_FRAMEBUFFER_CONSOLE

fbterm
fbterm

接上显示器,启动之后,就可以看到它在屏幕上慢悠悠地printk()了。。

一个小问题

内核初始化结束后,会统一将未用到的外设的时钟关掉;但是这个ltdc的驱动并没有向内核注册时钟,于是它最后就被关掉了。。。于是在drivers/clk/clk-stm32f4.c里为ltdc时钟添加CLK_IGNORE_UNUSED的属性。

static const struct stm32f4_gate_data 
    stm32f429_gates[] __initconst = {
    ...
    { STM32F4_RCC_APB2ENR, 26, "ltdc", "apb2_div",
      CLK_IGNORE_UNUSED },
};

仅使用simple-fb

这是最简单的方法:bootloader里面初始化显示器,然后给内核传参framebuffer的地址,从而不用那个庞大的DRM架构。初始化部分在bootloader里已讲述。

Device Drivers->Graphics support->Frame buffer Devices下找到Simple framebuffer support

simple-fb
simple-fb

在设备树的chosen节点下增加一个framebuffer节点:

/ {
    chosen {
        framebuffer0: framebuffer@83f00000 {
            compatible = "simple-framebuffer";
            reg = <0x83f00000 (640 * 480 * 2)>;
            width = <640>;
            height = <480>;
            stride = <(640 * 2)>;
            format = "r5g6b5";
            clocks = <&rcc 1 CLK_LCD>;
        };
    };
};

其中reg属性是framebuffer的地址,其余属性顾名思义。

显示驱动就可以塞进内核从而跑在片内flash里了。启动之后明显感到刷屏速度增加了数倍。

USB主机

STM32F429有两个USB 2.0,一个是全速USB,一个是全速、高速USB,二者片内集成了全速的phy,而后者需要外接phy芯片才能实现高速USB。为简单起见,uc-PC只引出了两个全速USB。虽然它们的寄存器组都非常相似,而且它们都使用Linux内核中相同的驱动,但是它们的表现不太一样。高速USB工作在全速USB下会有点不爽。

基本配置

跟很多SOC一样,stm32的很多外设都是Designware的IP,所以驱动比较通用;在drivers/usb/dwc2里面见到stm32的USB驱动竟诸如树莓派(BCM2835)之类的USB驱动并列时,不必惊讶。内核配置在Device Drivers->USB support下面找到DesignWare USB2 DRD Core Support

stm32 dw usb
stm32 dw usb

设备树里面添加两个USB节点:

&usbotg_hs {
    compatible = "st,stm32f4x9-fsotg";
    dr_mode = "host";
    pinctrl-0 = <&usbotg_fs_pins_b>;
    pinctrl-names = "default";
    status = "okay";
};

&usbotg_fs {
    compatible = "st,stm32f4x9-fsotg";
    dr_mode = "host";
    pinctrl-0 = <&usbotg_fs_pins_a>;
    pinctrl-names = "default";
    status = "okay";
};

但是按照默认代码,高速USB启动时候有错:

[4.320000] dwc2 40040000.usb: dwc2_core_reset() HANG! Soft Reset GRSTCTL=80000001

这是因为,dwc的USB HS在配置之前需要复位:通过置位GRSTCTL的第0位,然后等待硬件清零。stm32的USB HS默认是外接PHY芯片的高速模式,软件复位时候需要等PHY芯片。但是板子只有内置PHY。选择内置PHY需要置位GUSBCFG的第6位。

drivers/usb/dwc2/platform.c:dwc2_driver_probe()函数中,首先软件复位,然后在后面的drivers/usb/dwc2/hcd.c:dwc2_fs_phy_init()才设置为内置PHY。前面的软件复位等的是外部PHY,于是卡死,无论后面再怎么设置内置PHY。因此需要在前面先将其设置为全速USB模式:

    // 第418行前面,先设置为全速USB模式
    // 其实严格来说应该用writel接口的。。。
    *(uint32_t*)0x4004000c |= 0x00000040;
    msleep(1);
    dwc2_core_reset_and_force_dr_mode(hsotg);

然后就能启动了。虽然下面的printk还是有点问题,但是启动之后这个USB是没问题的,姑且就不管它了。

[4.950000] dwc2 40040000.usb: dwc2_wait_for_mode: Couldn't set host mode
[4.960000] dwc2 40040000.usb: DWC OTG Controller
[4.960000] dwc2 40040000.usb: new USB bus registered, assigned bus number 1
[4.970000] dwc2 40040000.usb: irq 50, io mem 0x40040000
[5.010000] hub 1-0:1.0: USB hub found
[5.020000] hub 1-0:1.0: 1 port detected

添加简单设备:USB串口

直接添加驱动就可以了,比方说PL2303驱动:

pl2303驱动配置
pl2303驱动配置

启动后插上PL2303串口,就可以有/dev/ttyUSB0了:

pl2303插入板子
pl2303插入板子

(其实USB hub是相当通用的233)

添加U盘驱动

需要先在Device Drivers->SCSI device support里面使能SCSI以及SCSI disk:

SCSI
SCSI

然后在Device Drivers->USB support下面使能USB Mass Storage:

USB storage
USB storage

还要有一个文件系统:比方说fatfs,在File systems->DOS/FAT/NT Filesystems下面:

VFAT
VFAT

而且还需要有一个字符编码,即所谓的”Native language support”,不然U盘会挂载失败。这里选用”NLS ISO 8859-1”:

nls
nls

插入U盘后就可以有以下printk:

[ 49.210000] usb 2-1: new full-speed USB device number 2 using dwc2
[ 49.460000] usb 2-1: not running at top speed; connect to a high speed hub
[ 49.480000] usb 2-1: New USB device found, idVendor=14cd, idProduct=1212
[ 49.490000] usb 2-1: New USB device strings: Mfr=1, Product=3, SerialNumber=2
[ 49.500000] usb 2-1: Product: Mass Storage Device
[ 49.510000] usb 2-1: Manufacturer: Generic
[ 49.520000] usb 2-1: SerialNumber: 121220130416
[112.900000] SCSI subsystem initialized
[112.990000] usb-storage 2-1:1.0: USB Mass Storage device detected
[113.010000] scsi host0: usb-storage 2-1:1.0
[113.020000] usbcore: registered new interface driver usb-storage
[114.080000] scsi 0:0:0:0: Direct-Access     Mass     Storage Device   1.00 PQ: 0 ANSI: 0 CCS
[120.110000] sd 0:0:0:0: [sda] 15523840 512-byte logical blocks: (7.95 GB/7.40 GiB)
[120.130000] sd 0:0:0:0: [sda] Write Protect is off
[120.140000] sd 0:0:0:0: [sda] Mode Sense: 03 00 00 00
[120.160000] sd 0:0:0:0: [sda] No Caching mode page found
[120.170000] sd 0:0:0:0: [sda] Assuming drive cache: write through
[120.200000] random: crng init done
[120.210000]  sda: sda1 sda2
[120.230000] sd 0:0:0:0: [sda] Attached SCSI removable disk

可见它已经识别到/dev/sda1、/dev/sda2了。这个U盘里是树莓派的rootfs,其中第一个分区是fat格式,可以挂载来看看:

/ # mount -t vfat /dev/sda1 /mnt/
[  233.320000] FAT-fs (sda1): Volume was not properly unmounted. Some data may be corrupt. Please run fsck.
/ # ls mnt
bcm2709-rpi-2-b.dtb  fixup.dat            start.elf
bcm2710-rpi-3-b.dtb  fixup_cd.dat         start_cd.elf
bootcode.bin         fixup_db.dat         start_db.elf
capture              fixup_x.dat          start_x.elf
cmdline.txt          kernel7.img
config.txt           overlays
/ # 

添加键盘驱动

键盘驱动也相当通用,直接配置它即可。在Device Drivers->HID support里面选择Generic HID driver,并且在USB HID drivers下面还要选USB HID transport layer

usb键盘
usb键盘

usb键盘
usb键盘

然后插上一个无线键盘(一定要无线键盘或者机械键盘,普通的有线键盘不行。。),就可以有下面的printk:

[  830.670000] usb 2-1: new full-speed USB device number 3 using dwc2
[  830.930000] usb 2-1: New USB device found, idVendor=24ae, idProduct=2010
[  830.940000] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[  830.950000] usb 2-1: Product: Rapoo 2.4G Wireless Device
[  830.960000] usb 2-1: Manufacturer: RAPOO
[  831.000000] input: RAPOO Rapoo 2.4G Wireless Device as /devices/platform/soc/50000000.usb/usb2/2-1/2-1:1.0/0003:24AE:2010.0001/input/input0
[  831.010000] hid-generic 0003:24AE:2010.0001: input,hidraw0: USB HID v1.10 Mouse [RAPOO Rapoo 2.4G Wireless Device] on usb-50000000.usb-1/input0
[  831.080000] input: RAPOO Rapoo 2.4G Wireless Device as /devices/platform/soc/50000000.usb/usb2/2-1/2-1:1.1/0003:24AE:2010.0002/input/input1
[  831.160000] hid-generic 0003:24AE:2010.0002: input,hiddev96,hidraw1: USB HID v1.10 Device [RAPOO Rapoo 2.4G Wireless Device] on usb-50000000.usb-1/input1
[  831.210000] input: RAPOO Rapoo 2.4G Wireless Device as /devices/platform/soc/50000000.usb/usb2/2-1/2-1:1.2/0003:24AE:2010.0003/input/input2
[  831.290000] hid-generic 0003:24AE:2010.0003: input,hidraw2: USB HID v1.10 Keyboard [RAPOO Rapoo 2.4G Wireless Device] on usb-50000000.usb-1/input2

此时敲击键盘,屏幕上的终端将会有显示:

键盘orz
键盘orz

其实Linux的input事件是全局性的,因此只要驱动配好了,就有反应了。

为啥不能用普通的有线键盘?

stm32的USB在Linux下的驱动有个很大的问题:它不识别低速设备。。。整天枚举失败。。。这估计是stm32跑Linux的性能太低了,以至于设备跑得比stm32都要快,于是就说设备不接受地址云云。。而低端的有线键盘都是低速设备,于是枚举失败。。

[ 1506.090000] usb 2-1: new low-speed USB device number 6 using dwc2
[ 1506.310000] usb 2-1: device descriptor read/64, error -71

SD卡

emcraft的4.2内核中,SD卡驱动直接用arm通用的驱动”arm,primecell”,这个compatible在drivers/of/platform.c里面匹配,并生成一个amba总线。另外它在drivers/mmc/host/mmci.c里面实现了一个variant_stm32f4,从而接入Linux的mmc驱动栈中。

按理说主流的Linux 4.13内核也是可以这么干的,但是这里的drivers/mmc/host/mmci.c的数据结构跟前者并不完全相同,而且逻辑处理也不同了。这导致SD卡整天不能识别。。

方便起见,直接换能用的代码

将emcraft的mmci.c、mmci.h复制过来直接用。。。
内核配置:选择ARM AMBA Multimedia Card Interface support以及MMC block device driver

mmc配置
mmc配置

设备树增加mmc节点,基本照抄emcraft的,不过并没用DMA,因为性能比较低下,DMA很容易下溢死掉:

sdio:sdi@40012C00 {
    compatible = "arm,primecell";
    reg = <0x40012C00 0x400>;
    interrupts = <49>;
    max-frequency = <25000000>;
    bus-width = <4>;
    voltage-ranges = <3200 3300 3300 3400>;
    clocks = <&rcc 0 171>;
    clock-names = "apb_pclk";
    arm,primecell-periphid = <0x40480180>;
    status = "disabled";
};

...
&sdio {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&sdio_pins>;
    cd-gpio = <&gpioh 15 GPIO_ACTIVE_LOW>;
};

其中cd-gpio是卡识别的GPIO。

挂载SD卡

插卡后,将有以下的printk:

[ 5492.300000] mmc0: host does not support reading read-only switch, assuming write-enable
[ 5492.310000] mmc0: new SD card at address 0001
[ 5492.320000] mmcblk0: mmc0:0001  971 MiB 
[ 5492.340000]  mmcblk0: p1 p2
/ # ls /dev/mmcblk0*
/dev/mmcblk0    /dev/mmcblk0p1  /dev/mmcblk0p2
/ # 

此时就可以正常挂载它了。不过有时候会出现EIO错误;发出EIO的地方是mmci.c:mmci_data_irq第995行左右的 if (status & MCI_RXOVERRUN),这是SDIO的FIFO溢出了,说明主控取数据太慢了。。解决的办法是开硬件流控制,当sdio发现fifo溢出时候会卡一下时钟,免得sd卡跑的太快。这就需要在variant_stm32f4里面加一个控制位:

.clkreg_enable          = MCI_ST_UX500_HWFCEN,

这是SDIO_CLKCR的第14位。

有时候也会有EILSEQ错误,也是在mmci.c:mmci_data_irq里面发出的,起因是CRC错误,这可能是因为走线不太好或者接触不良啥的。于是降低时钟频率:调高SDIO_CLKCR的低8位分频数,于是variant_stm32f4的clkreg_enable变成:

.clkreg_enable          = MCI_ST_UX500_HWFCEN | 0x00000006,

然后就没错了。。。

摄像头

Linux里面集成了STM32的DCMI驱动,直接用就行了。

基本配置

Device Drivers->Multimedia support->V4L platform devices里面使能DCMI的驱动:

STM32 DCMI
STM32 DCMI

然后添加摄像头驱动。记得要先取消掉Autoselect ancillary drivers的选项,下面才有一系列的摄像头驱动可选:

摄像头驱动
摄像头驱动

不妨添加OV2640的驱动。这是一个200万像素的摄像头,可以输出RGB、YUV、JPEG格式的照片。

OV2640驱动
OV2640驱动

然后设备树中,在I2C节点下面添加OV2640的节点:

&i2c1 {
    status = "okay";

    ov2640: camera@30 {
        compatible = "ovti,ov2640";
        reg = <0x30>;
        clocks = <&clk_ext_camera>;
        clock-names = "xvclk";
        status = "okay";
        port {
            // 指向下面的dcmi_0 endpoint
            ov2640_0: endpoint {
                remote-endpoint = <&dcmi_0>;
            };
        };
    };
};

&dcmi {
    status = "okay";

    port {
        dcmi_0: endpoint {
            // 指向上面的ov2640 endpoint
            remote-endpoint = <&ov2640_0>;
            bus-width = <8>;
            hsync-active = <0>;
            vsync-active = <0>;
            pclk-sample = <1>;
        };
    };
};

配置基本上照抄stm32429i-eval的配置。

启动之后可以看到摄像头的printk:

[    5.600000] stm32f4-i2c 40005400.i2c: STM32F4 I2C driver registered
[    5.760000] Linux video capture interface: v2.00
[    5.870000] stm32-dcmi 50050000.dcmi: Probe done
[    5.970000] ov2640 0-0030: ov2640 Product ID 26:42 Manufacturer ID 7f:a2
[    5.990000] i2c i2c-0: OV2640 Probed

v4l2应用程序测试

v4l2有个官方测试程序capture.c,它将摄像头数据写入文件中。可以改写一下,将裸RGB数据写到/dev/fb0中,那么就可以在显示器上看到摄像头的数据了。比方说这样写:

// 先打开/dev/fb0
fp = fopen("/dev/fb0", "w");
framebuffer = malloc(640*480*2);

// 在主循环里面将数据写入framebuffer
static void process_image(const void *p, int size)
{
    short *fb = framebuffer, *src = p;
    if (out_buf) {
        for(int y = 0; y < 240; y++)
            for(int x = 0; x < 320; x++)
                fb[y*2*640 + x*2] = src[y*320 + x];
        fseek(fp, 0, SEEK_SET);
        fwrite(fb, 640*480*2, 1, fp);
    }

    fflush(stderr);
    fprintf(stderr, ".");
    fflush(stdout);
}

编译之后,使用.capture -o -f -c 10,即是获取10张照片并写入framebuffer。可以在显示器上看到照片:

摄像头测试
摄像头测试

不过由于STM32的DMA最多只能搬运64k个单位的数据,以32字为单位的话最多就是256kb大小的数据。于是摄像头只能设置为320x240大小。并且由于应用程序在SDRAM上跑,性能低下,以至于两秒钟才能出一张图片。。。而且SDRAM的带宽太小,拍照时还会时常说DMA下溢orz。。。据ST的手册推荐的话,应该利用DMA的双缓冲特性从而突破64k的限制,不过内核中并没有实现这一点。

yaffs2

yaffs已经是一个相当老的文件系统了。。。它在几十上百MB的SLC上面跑到很欢快,但是并不太适用于MLC、TLC之类的几GB以上的大容量nand flash。不过对于uc-PC来说已经足够了。yaffs2适应于大页(2kb)的nand flash,yaffs适应于小页(512B)的nand flash。yaffs2声称它可工作在yaffs模式下面从而向前适应(但是我实验结果发现它并不如此)。

yaffs可以从它官网上下载

打补丁

根据它的README-linux,使用以下命令去给内核打补丁:

./patch-ker.sh  c m <内核目录>

然后内核配置时候就可以有yaffs的选项了:

yaffs配置
yaffs配置

yaffs2已经可以支持4.9的内核了。不过在4.13内核上编译有问题,提示CURRENT_TIME这个宏未定义。发现这是因为Linux打算解决y2038的千年虫问题,需要改用别的计时方式。我直接用了current_kernel_time()代替了。

接下来要在内核里面添加nand flash的驱动。为方便起见,使用platform-nand驱动,即CONFIG_MTD_NAND_PLATFORM

platform nand驱动
platform nand驱动

仿照arch/arm/mach-omap1/board-h2.c,在arch/arm/mach-stm32/board-dt.c里面定义分区表以及驱动信息。

试探性的定义了三个分区:

/* 分区表 */
static struct mtd_partition stm32_nand_partitions[] = {
    {
        .name       = "first",
        .offset     = 0,
        .size       = 128 * 1024,
        .mask_flags = MTD_WRITEABLE,    /* force read-only */
    },
    {
        .name       = "second",
        .offset     = MTDPART_OFS_APPEND,
        .size       = 256 * 1024,
        .mask_flags = MTD_WRITEABLE,    /* force read-only */
    },
    {
        .name       = "rootfs",
        .offset     = MTDPART_OFS_APPEND,
        .size       = 120 * SZ_1M,
    },
};

然后是platform data:

static struct platform_nand_data stm32_nand_platdata = {
    .chip = {
        // 分区表
        .nr_partitions = ARRAY_SIZE(stm32_nand_partitions),
        .partitions = stm32_nand_partitions,
    },
    .ctrl = {
        // 读写驱动程序
        .cmd_ctrl  = fmc_send_cmd,
        .dev_ready = fmc_read_rb,
    },
};

接着是platform device:

static struct platform_device stm32_nand_device = {
    .name = "gen_nand",
    .dev  = {
        .platform_data = &stm32_nand_platdata,
    },
};

最后,在模块构造函数里面注册platform device:

static void __init board_stm32_ucpc_init(void)
{
    printk("STM32 UCPC: nand platform device");
    platform_add_devices(stm32_devices, ARRAY_SIZE(stm32_devices));
}

启动之后,可以看到识别到三个分区:

[    0.970000] nand: device found, Manufacturer ID: 0xec, Chip ID: 0xf1
[    0.980000] nand: Samsung NAND 128MiB 3,3V 8-bit
[    0.980000] nand: 128 MiB, SLC, erase size: 128 KiB, page size: 2048, OOB size: 64
[    0.990000] Scanning device for bad blocks
[    1.090000] Creating 3 MTD partitions on "gen_nand.0":
[    1.100000] 0x000000000000-0x000000020000 : "first"
[    1.120000] 0x000000020000-0x000000060000 : "second"
[    1.150000] 0x000000060000-0x000007860000 : "rootfs"

yaffs作为根文件系统

查看dmesg,可以看到有Creating xxx MTD partitions on xxx的信息。根据它出现的时间先后顺序,这些块设备依次形成/dev/mtdblock0/dev/mtdblock1/dev/mtdblock2等等。将含有rootfs的mtd分区传参给内核。

但是在这之前先得将rootfs烧录进nand flash。我使用了最简单的方法:先uboot通过tftp启动一个跑在SDRAM上的内核,挂载mtd分区以及nfs,然后从nfs上面拷贝rootfs到mtd分区里面。。。然后改写kernel command line,将mtd分区作为rootfs传参。