19. Accessing Hardware Devices
19.1 Objectives
- Learn how to access hardware devices.
19.2 Setup
Go to the $HOME/embedded-linux-bbb-labs/hardware/
directory, which provides useful files for
this lab.
However, we will go on booting the system through NFS, using the root filesystem built by the
previous lab.
19.3 Exploring /dev
Start by exploring /dev/
on your target system.
# ls /dev/
bus ram1 tty26 tty59
console ram10 tty27 tty6
cpu_dma_latency ram11 tty28 tty60
full ram12 tty29 tty61
gpiochip0 ram13 tty3 tty62
gpiochip1 ram14 tty30 tty63
gpiochip2 ram15 tty31 tty7
gpiochip3 ram2 tty32 tty8
hwrng ram3 tty33 tty9
i2c-0 ram4 tty34 ttyS0
i2c-2 ram5 tty35 ttyS1
kmsg ram6 tty36 ttyS2
loop-control ram7 tty37 ttyS3
loop0 ram8 tty38 ttyS4
loop1 ram9 tty39 ttyS5
loop2 random tty4 ubi_ctrl
loop3 tty tty40 urandom
loop4 tty0 tty41 vcs
loop5 tty1 tty42 vcs1
loop6 tty10 tty43 vcs2
loop7 tty11 tty44 vcs3
mem tty12 tty45 vcs4
mmcblk0 tty13 tty46 vcsa
mmcblk0p1 tty14 tty47 vcsa1
mmcblk0p2 tty15 tty48 vcsa2
mmcblk0p3 tty16 tty49 vcsa3
mmcblk1 tty17 tty5 vcsa4
mmcblk1boot0 tty18 tty50 vcsu
mmcblk1boot1 tty19 tty51 vcsu1
mmcblk1p1 tty2 tty52 vcsu2
mmcblk1rpmb tty20 tty53 vcsu3
null tty21 tty54 vcsu4
port tty22 tty55 vga_arbiter
ptmx tty23 tty56 zero
ptp0 tty24 tty57
ram0 tty25 tty58
Here are a few noteworthy device files that you will see:
-
Terminal devices: devices starting with
tty
.
Terminals are user interfaces taking text as input and producing text as output, and are typically used by interactive shells.
In particular, you will find console which matches the device specified throughconsole=
in the kernel command line (U-Boot'sbootargs
).
You will also find thettyS0
device file we used for the emulated debug serial port. -
Pseudo-terminal devices: devices starting with
pty
, used when you connect through SSH for example.
Those are virtual devices, but there are so many in/dev/
that we wanted to give a description here. -
MMC devices and partitions: devices starting with
mmcblk
.
You should here recognize the MMC devices on your system, and the associated partitions. -
If you have a real board (not QEMU) and a USB pen drive, you could plug it in, and if your kernel was built with USB host and mass storage support, you should see a new
sda
device appear, together with thesdaX
devices for its partitions.
Don't hesitate to explore /dev/
on your workstation too!
$ ls /dev/
autofs loop1 rtc0 tty2 tty47 ttyS15 vboxguest
block loop10 sda tty20 tty48 ttyS16 vboxuser
bsg loop11 sda1 tty21 tty49 ttyS17 vcs
btrfs-control loop12 sdb tty22 tty5 ttyS18 vcs1
bus loop13 sdb1 tty23 tty50 ttyS19 vcs2
cdrom loop14 sdc tty24 tty51 ttyS2 vcs3
char loop2 sg0 tty25 tty52 ttyS20 vcs4
console loop3 sg1 tty26 tty53 ttyS21 vcs5
core loop4 sg2 tty27 tty54 ttyS22 vcs6
cpu loop5 sg3 tty28 tty55 ttyS23 vcsa
cpu_dma_latency loop6 shm tty29 tty56 ttyS24 vcsa1
cuse loop7 snapshot tty3 tty57 ttyS25 vcsa2
disk loop8 snd tty30 tty58 ttyS26 vcsa3
dma_heap loop9 sr0 tty31 tty59 ttyS27 vcsa4
dri loop-control stderr tty32 tty6 ttyS28 vcsa5
ecryptfs mapper stdin tty33 tty60 ttyS29 vcsa6
fb0 mcelog stdout tty34 tty61 ttyS3 vcsu
fd mem tty tty35 tty62 ttyS30 vcsu1
full mqueue tty0 tty36 tty63 ttyS31 vcsu2
fuse net tty1 tty37 tty7 ttyS4 vcsu3
hidraw0 null tty10 tty38 tty8 ttyS5 vcsu4
hpet nvram tty11 tty39 tty9 ttyS6 vcsu5
hugepages port tty12 tty4 ttyprintk ttyS7 vcsu6
hwrng ppp tty13 tty40 ttyS0 ttyS8 vfio
i2c-0 psaux tty14 tty41 ttyS1 ttyS9 vga_arbiter
initctl ptmx tty15 tty42 ttyS10 udmabuf vhci
input pts tty16 tty43 ttyS11 uhid vhost-net
kmsg random tty17 tty44 ttyS12 uinput vhost-vsock
log rfkill tty18 tty45 ttyS13 urandom zero
loop0 rtc tty19 tty46 ttyS14 userio zfs
19.4 Exploring /sys
The next thing you can explore is the sysfs filesystem.
A good place to start is /sys/class/
, which exposes devices classified by the kernel frameworks which manage them.
# ls /sys/class/
ata_device extcon mdio_bus power_supply scsi_host
ata_link firmware mem pps spi_master
ata_port gpio misc ptp thermal
backlight graphics mmc_host pwm tty
bdi i2c-adapter mtd regulator ubi
block i2c-dev net remoteproc udc
devcoredump input pci_bus rtc vc
devlink iommu pci_epc scsi_device vtconsole
dma lcd phy scsi_disk wakeup
For example, go to /sys/class/net/
, and you will see all the networking interfaces on your system, whether they are internal, external or virtual ones.
Find which subdirectory corresponds to the network connection to your host system, and then check device properties such as:
-
speed
: will show you whether this is a gigabit or hundred megabit interface. -
address
: will show the device MAC address. No need to get it from a complex command! -
statistics/rx_bytes
will show you how many bytes were received on this interface.
Don't hesitate to look for further interesting properties by yourself!
# ls /sys/class/net/eth0/
addr_assign_type flags phys_port_name
addr_len gro_flush_timeout phys_switch_id
address ifalias power
broadcast ifindex proto_down
carrier iflink queues
carrier_changes link_mode speed
carrier_down_count mtu statistics
carrier_up_count name_assign_type subsystem
dev_id napi_defer_hard_irqs testing
dev_port netdev_group threaded
device operstate tx_queue_len
dormant phydev type
duplex phys_port_id uevent
# cat /sys/class/net/eth0/speed
100
# cat /sys/class/net/eth0/address
54:4a:16:be:9e:ae
# cat /sys/class/net/eth0/statistics/rx_bytes
925186
You can also check whether /sys/class/thermal/
exists and is not empty on your system.
That's the thermal framework, and it allows to access temperature measures from the thermal sensors on your system.
Next, you can now explore all the buses (virtual or physical) available on your system, by checking the contents of /sys/bus/
.
In particular, go to /sys/bus/mmc/devices/
to see all the MMC devices on your system.
Go inside the directory for the first device and check several files (for example):
-
serial
: the serial number for your device. -
preferred_erase_size
: the preferred erase block for your device. It's recommended that partitions start at multiples of this size. -
name
: the product name for your device. You could display it in a user interface or log file, for example.
# ls /sys/bus/
clockevents genpd mipi-dsi pci-epf soc
clocksource gpio mmc platform spi
container hid mmc_rpmb scsi usb
cpu i2c nvmem sdio virtio
event_source mdio_bus pci serial workqueue
# ls /sys/bus/mmc/devices/
mmc0:0001 mmc1:0001
# ls /sys/bus/mmc/devices/mmc0:0001/
block hwrev scr
cid manfid serial
csd name ssr
date ocr subsystem
driver oemid type
dsr power uevent
erase_size preferred_erase_size
fwrev rca
# cat /sys/bus/mmc/devices/mmc0:0001/serial
0x000015f1
# cat /sys/bus/mmc/devices/mmc0:0001/preferred_erase_size
4194304
# cat /sys/bus/mmc/devices/mmc0:0001/name
SD16G
Don't hesitate to spend more time exploring /sys/
on your system!
19.5 Driving GPIOs
At this stage, we can only explore GPIOs through the legacy interface in /sys/class/gpio/
, because the libgpiod
interface commands are provided through a dedicated project which we have to build separately, and BusyBox does not provide a re-implementation for the libgpiod
tools.
In a later lab, we will build libgpiod
tools which use the modern /dev/gpiochipX
interface.
The first thing to do is to enable this legacy interface by enabling CONFIG_GPIO_SYSFS
in the kernel configuration.
Also make sure debugfs is enabled (CONFIG_DEBUG_FS
and CONFIG_DEBUG_FS_ALLOW_ALL
).
After rebooting the new kernel, the first thing to do is to mount the debugfs filesystem.
Then, you can check information about available GPIOs banks and which GPIOs are already in use.
# mount -t debugfs debugfs /sys/kernel/debug/
# cat /sys/kernel/debug/gpio
gpiochip0: GPIOs 0-31, parent: platform/4804c000.gpio, gpio-0-31:
gpio-0 (P8_25 [mmc1_dat0] )
gpio-1 ([mmc1_dat1] )
...
gpio-30 (P8_21 [emmc] )
gpio-31 (P8_20 [emmc] )
gpiochip1: GPIOs 32-63, parent: platform/481ac000.gpio, gpio-32-63:
gpio-32 (P9_15B )
gpio-33 (P8_18 )
...
gpio-62 ([mmc0_clk] )
gpio-63 ([mmc0_cmd] )
gpiochip2: GPIOs 64-95, parent: platform/481ae000.gpio, gpio-64-95:
gpio-64 ([mii col] )
gpio-65 ([mii crs] )
...
gpio-94 (NC )
gpio-95 (NC )
gpiochip3: GPIOs 96-127, parent: platform/44e07000.gpio, gpio-96-127:
gpio-96 ([mdio_data] )
gpio-97 ([mdio_clk] )
...
gpio-126 (P9_11 [uart4_rxd] )
gpio-127 (P9_13 [uart4_txd] )
We are going to use one of the free GPIOs on the expansion headers of the board, which is not already used by another device.
Take one of the M-M breadboard wires and:
- Connect one end to pin 12 of connector P9.
- Connect the other end to pin 1 (
DGND
) of connector P9.
If you check the description of the P9 connector on the board
System Reference Manual,
you can see that pin 12 is now called GPIO1_28
instead of GPIO_60
in the above diagram.
This pin is already configured as a GPIO by default — no need to change pin muxing to use this pin as a GPIO.
If you get back to the contents of /sys/kernel/debug/gpio/
, you'll recognize the association between gpio-28
on GPIO pin bank 0
(gpiochip0
) and header pin P9_12
.
That's very useful information, but you don't have this level of details for all boards, unfortunately.
We now have everything we need to drive this GPIO using the legacy interface. First, let's enable it.
# cd /sys/class/gpio/
# ls -1
export
gpiochip0
gpiochip32
gpiochip64
gpiochip96
unexport
# echo 28 > export
# ls -1
export
gpio28
gpiochip0
gpiochip32
gpiochip64
gpiochip96
unexport
If indeed the pin is still available, this should create a new gpio28
file should appear in /sys/class/gpio/
.
We can now configure this pin as input, and check its value
.
You could use this GPIO to add a button switch to your board, for example.
The value should be 0
as the pin is connected to a ground level.
Now, let's connect our GPIO pin to pin 3 (VDD 3.3 V) of connector P9. Check the above diagram if needed.
Let's check the value
again. The value is 1
because our pin is connected to a 3.3 V level now.
Note that you could also configure the pin as output and set its value through the value
file.
This way, you could add an external LED to your board, for example.
Before moving on to the next section, you can also check /sys/kernel/debug/gpio/
again, and see that gpio-28
is now in use, through the sysfs interface, and is configured as an input pin.
When you're done, you can see your GPIO free.
19.6 Driving LEDs
First, make sure your kernel is compiled with:
LEDS_CLASS=y
LEDS_GPIO=y
LEDS_TRIGGER_TIMER=y
$ cd "$LAB_PATH/../kernel/linux/"
$ cp "$LAB_PATH/../tinysystem/kernel-busybox.config" .config
$ make menuconfig
$ cp .config "$LAB_PATH/kernel-leds.config"
$ TC_NAME="arm-training-linux-uclibcgnueabihf"
$ TC_BASE="$HOME/x-tools/$TC_NAME"
$ export PATH="$TC_BASE/bin:$PATH"
$ export CROSS_COMPILE=arm-linux-
$ export MAKEFLAGS=-j$(nproc)
$ export ARCH=arm
$ make
$ cp arch/arm/boot/zImage /srv/tftp/zImage-with-LEDs
$ cp arch/arm/boot/zImage /srv/tftp/zImage
Then, go to /sys/class/leds/
to see all the LEDs that you are allowed to control.
# cd /sys/class/leds/
# ls -1
beaglebone:green:heartbeat
beaglebone:green:mmc0
beaglebone:green:usr2
beaglebone:green:usr3
mmc0::
mmc1::
Let's control the LED called beaglebone:green:heartbeat
.
Go into the directory for this LED, and check its trigger
(what routine is used to drive its value),
# cd beaglebone:green:heartbeat/
# triggers=$(cat trigger)
# for t in $triggers; do echo $t; done
[none]
kbd-scrolllock
kbd-numlock
kbd-capslock
kbd-kanalock
kbd-shiftlock
kbd-altgrlock
kbd-ctrllock
kbd-altlock
kbd-shiftllock
kbd-shiftrlock
kbd-ctrlllock
kbd-ctrlrlock
timer
cpu
cpu0
mmc0
mmc1
As you can see, there are many triggers to choose from, the current being none
.
You can directly control the LED without a trigger:
You could also use the timer trigger to light the LED with specified time on and time off.
You can disable all triggers by:
19.7 Managing I2C bus and devices
19.7.1 Enabling I2C bus
The next thing we want to do is connect a Nintendo Nunchuk joystick to an I2C bus on our board. The I2C bus is very frequently used to connect all sorts of external devices. That's why we're covering it here.
As shown on the picture below, the BeagleBone Black has two I2C busses available on its expansion headers: I2C1 and I2C2. Another one exists (I2C0), but it's not available on the external headers.
In this lab, we will try to use I2C1 on P9 pins 17 and 18, because it's more interesting to use than I2C2, which is already enabled by default.
So, let's see which I2C buses are already enabled:
# i2cdetect -l
i2c-2 i2c OMAP I2C adapter I2C adapter
i2c-0 i2c OMAP I2C adapter I2C adapter
Here you can see that I2C1 is missing.
As the bus numbering scheme in Linux doesn't always match the one on the datasheets, let's check the base addresses of the registers of these controllers:
# ls -l /sys/bus/i2c/devices/i2c-*
lrwxrwxrwx 1 0 Jan 1 01:15 /sys/bus/i2c/devices/i2c-0 -> \
../../../devices/platform/ocp/44c00000.interconnect/\
44c00000.interconnect:segment@200000/44e0b000.target-module/44e0b000.i2c/i2c-0
lrwxrwxrwx 1 0 Jan 1 01:15 /sys/bus/i2c/devices/i2c-2 -> \
../../../devices/platform/ocp/48000000.interconnect/\
48000000.interconnect:segment@100000/4819c000.target-module/4819c000.i2c/i2c-2
That's not completely straighforward, but you can suppose that:
- I2C0 is at address
0x44e0b000
- I2C2 is at address
0x4819c000
Now let's double check the addressings by looking at the TI AM335x SoC datasheet, in the L4_WKUP Peripheral Memory Map section:
- I2C0 is at address
0x44e0b000
- I2C1 is at address
0x4802a000
- I2C2 is at address
0x4819c000
So, we are lucky that i2c-0
in Linux corresponds to I2C0 in the datasheet, and that i2c-2
corresponds to I2C2. We're just missing i2c-1
.
19.7.2 Customizing Device Tree
Fortunately, I2C1 is already defined in the one of the DTS includes used by the Device Tree for our board. In our case, that's in arch/arm/boot/dts/am33xx-l4.dtsi
.
Look by yourself in this file, and you will find its definition, but with status = "disabled";
.
This means that this I2C controller is not enabled yet, and it's up to boards using it to do so.
We could modify the arch/arm/boot/dts/am335x-boneblack.dts
file for our board, but that's not a very good idea as this file is maintained by the kernel developers.
The changes that you make could collide with future changes made by the maintainers for this file.
A more futureproof idea is to create a new Device Tree file which includes the standard one, and adds custom definitions. So, create a new arch/arm/boot/dts/am335x-boneblack-custom.dts
file containing:
/dts-v1/;
#include "am335x-boneblack.dts"
&i2c1 {
status = "okay";
};
As you can see, it's also possible to include dts
files, and not only dtsi
ones.
Modify the arch/arm/boot/dts/Makefile
file to add your custom Device Tree, and then have it compiled (make dtbs
).
...
dtb-$(CONFIG_SOC_AM33XX) += \
am335x-baltos-ir2110.dtb \
am335x-baltos-ir3220.dtb \
am335x-baltos-ir5221.dtb \
am335x-base0033.dtb \
am335x-bone.dtb \
am335x-boneblack.dtb \
am335x-boneblack-custom.dtb \
am335x-boneblack-wireless.dtb \
am335x-boneblue.dtb \
am335x-bonegreen.dtb \
...
Update the board and reboot.
$ make dtbs
DTC arch/arm/boot/dts/am335x-boneblack-custom.dtb
$ cp arch/arm/boot/dts/am335x-boneblack-custom.dtb /srv/tftp/
...
Hit any key to stop autoboot: 0
=> setenv bootcmd_tftp "tftp 0x81000000 zImage; tftp 0x82000000 am335x-boneblack-custom.dtb; bootz 0x81000000 - 0x82000000"
=> setenv bootcmd $bootcmd_tftp
=> saveenv
=> reset
Back to the running system, we can now see that there is one more I2C bus:
# i2cdetect -l
i2c-1 i2c OMAP I2C adapter I2C adapter
i2c-2 i2c OMAP I2C adapter I2C adapter
i2c-0 i2c OMAP I2C adapter I2C adapter
Run the below command to confirm that the new bus has the same address as in the datasheet (0x4802a000
):
# ls -l /sys/bus/i2c/devices/i2c-1
lrwxrwxrwx 1 0 Jan 1 00:02 /sys/bus/i2c/devices/i2c-1 -> \
../../../devices/platform/ocp/48000000.interconnect/\
48000000.interconnect:segment@0/4802a000.target-module/4802a000.i2c/i2c-1
Now, let's use i2cdetect
's capability to probe a bus for devices.
Let's start by the bus associated to i2c-0
:
# i2cdetect -y -r 0
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- UU -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- 34 -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
We can see three devices on this internal bus:
-
One at address
0x24
, indicated byUU
, which means that there is a kernel driver actively driving this device. -
Two other devices at addresses
0x34
and0x50
. We just know that they are currently not bound to a kernel driver.
Now try to probe I2C1:
# i2cdetect -y -r 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: [ 469.430509] omap_i2c 4802a000.i2c: timeout waiting for bus ready
...
You will see that the command will fail to connect to the bus. That's because the corresponding signals are not exposed yet to the outside connectors through pin muxing.
So, get back to your custom Device Tree and add pin muxing definitions for I2C1 (we took them from a device tree from another board with the same CPU: arch/arm/boot/dts/am335x-evm.dts
) and refer to these definitions in the i2c1
node through the pinctrl-names
and pinctrl-0
properties:
/dts-v1/;
#include "am335x-boneblack.dts"
&am33xx_pinmux {
i2c1_pins: pinmux_i2c1_pins {
pinctrl-single,pins = <
AM33XX_PADCONF(AM335X_PIN_SPI0_CS0, PIN_INPUT_PULLUP, MUX_MODE2) /* spi0_cs0.i2c1_scl */
AM33XX_PADCONF(AM335X_PIN_SPI0_D1, PIN_INPUT_PULLUP, MUX_MODE2) /* spi0_d1.i2c1_sda */
>;
};
};
&i2c1 {
pinctrl-names = "default";
pinctrl-0 = <&i2c1_pins>;
status = "okay";
};
You can understand the above values thanks to the pin muxing diagram for connector P9, which was extracted from the board System Reference Manual:
-
AM335X_PIN_SPI0_CS0
andAM335X_PIN_SPI0_D1
are the offsets of the registers controlling pin muxing for the corresponding pins of the SoC package. -
PIN_INPUT_PULLUP
is one of the supported options for these pins. They integrate the pull-up resistors expected for an I2C bus (typically by external resistors). -
MUX_MODE2
corresponds to MODE2, to get I2C1_SCL and I2C1_SDA signals on such pins.
Recompile your Device Tree and reboot.
$ make dtbs
DTC arch/arm/boot/dts/am335x-boneblack-custom.dtb
$ cp arch/arm/boot/dts/am335x-boneblack-custom.dtb /srv/tftp/
You should now be able to probe your bus:
# i2cdetect -y -r 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
No device is detected yet, because this bus is just used for external devices. It's time to add one though.
19.7.3 Adding I2C device
Let's connect the Nunchuk to the I2C1 bus on the board. To know its pinout, please refer to the excerpt found on the Bootlin website.
Connections on the Nunchuk depend on the adapter you're using (if any); please refer to its own pinout.
If you didn't make any mistakes, your new device should be detected at address 0x52
:
# i2cdetect -y -r 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- 52 -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
Later we're going to compile an out-of-tree kernel module to support this device.
19.8 Plugging an audio USB headset
In the next labs, we are going to play audio using a USB audio headset. Let's see whether our kernel supports such hardware by plugging the USB headset.
Before plugging the device, look at the output of lsusb
:
Now, when you plug the USB headset, a number of messages should appear on the console, and running lsusb
again should show an additional device:
The device of vendor ID 046d
and product ID 0a38
has appeared.
Of course, this depends on the actual USB audio device that you used.
The device also appears in /sys/bus/usb/devices/
, in a directory whose name depends on the topology of the USB bus.
When the device is plugged in the kernel messages show:
# dmesg
...
usb 1-1: new full-speed USB device number 2 using musb-hdrc
usb 1-1: New USB device found, idVendor=046d, idProduct=0a38, bcdDevice= 1.15
usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-1: Product: Logitech USB Headset H340
usb 1-1: Manufacturer: Logitech Inc.
...
So if we go in /sys/bus/usb/devices/1-1/
, we get the sysfs representation of this USB device:
# cd /sys/bus/usb/devices/1-1/
# cat idVendor idProduct busnum devnum product
046d
0a38
1
2
Logitech USB Headset H340
However, while the USB device is detected, we currently do not have any driver for this device, so no actual sound card is detected.
19.9 In-tree kernel modules
Go back to the kernel source directory.
The Linux kernel has a generic driver supporting all USB audio devices supporting the standard USB audio class.
This driver can be enabled using the SND_USB_AUDIO
configuration option.
Look for this parameter in the kernel configuration, and you should find that it is already enabled as a module.
So, instead of compiling the corresponding driver as a built-in, that's a good opportunity to practice with kernel modules.
So, compile your modules:
Then, following details given in the lectures, install the modules in our NFS root filesystem
($HOME/embedded-linux-bbb-labs/tinysystem/nfsroot/
).
Also make sure to update the kernel image (make zImage
), and reboot the board.
$ export INSTALL_MOD_PATH="$LAB_PATH/../tinysystem/nfsroot"
$ make
$ make modules_install
...
INSTALL /home/me/embedded-linux-bbb-labs/hardware/../tinysystem/nfsroot/lib/modules/5.15.104/kernel/sound/usb/snd-usbmidi-lib.ko
DEPMOD /home/me/embedded-linux-bbb-labs/hardware/../tinysystem/nfsroot/lib/modules/5.15.104
$ cp arch/arm/boot/zImage /srv/tftp/zImage
If using git for the Linux kernel source tree, due to the changes we have made to the kernel source code, the kernel version is now
5.15.<x>-dirty
, thedirty
keyword indicating that the git working tree has uncommitted changes.
The modules are therefore installed in/lib/modules/5.15.<x>-dirty/
, and the version of the running Linux kernel must match this.
After rebooting, try to load the module that we need (snd-usb-audio
).
By running lsmod
, see all the module dependencies that were loaded too..
You can also see that a new USB device driver in /sys/bus/usb/drivers/snd-usb-audio
. This directory shows which USB devices are bound to this driver.
# uname -r
5.15.104-dirty
# modprobe snd-usb-audio
mc: Linux media interface: v0.10
usbcore: registered new interface driver snd-usb-audio
# lsmod
Module Size Used by
snd_usb_audio 217088 0
snd_hwdep 16384 1 snd_usb_audio
snd_usbmidi_lib 28672 1 snd_usb_audio
mc 36864 1 snd_usb_audio
snd_rawmidi 28672 1 snd_usbmidi_lib
snd_pcm 106496 1 snd_usb_audio
snd_timer 28672 1 snd_pcm
snd 61440 6 snd_usb_audio,snd_hwdep,snd_usbmidi_lib,snd_rawmidi,snd_pcm,snd_timer
soundcore 16384 1 snd
You can check that /proc/asound/
now exists (thanks to loading modules for ALSA, the Linux sound subsystem), and that one sound card is available:
# ls -1 /proc/asound/
H340
card0
cards
devices
hwdep
modules
oss
pcm
timers
version
# cat /proc/asound/cards
0 [H340 ]: USB-Audio - Logitech USB Headset H340
Logitech Inc. Logitech USB Headset H340 at usb-musb-hdrc.1-1, full speed
Check also the /dev/snd/
directory, which should now contain some character device files.
These will be used by the user-space libraries and applications to access the audio devices.
Modify your startup scripts so that the snd-usb-audio
module is always loaded at startup.
$ cd "$LAB_PATH/../tinysystem/nfsroot/"
$ cat > etc/init.d/rcS <<'EOF'
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t devtmpfs dev /dev
/usr/sbin/httpd -h /www/
modprobe snd-usb-audio
EOF
We cannot test the sound card yet, as we will need to build some software first. Be patient, this is coming soon!
19.10 Out-of-tree kernel modules
The next device we want to support is the I2C Nintendo Nunchuk. There is a driver in the kernel to support it when connected to a Wiimote controller, but there is no such driver to support it as an I2C device.
Fortunately, one is provided in $HOME/embedded-linux-bbb-labs/hardware/data/nunchuk/nunchuk.c
.
You can check Bootlin's Linux kernel and driver development course to learn how to implement all sorts of device drivers for Linux.
Go to this directory, and compile the out-of-tree module as follows:
$ cd "$LAB_PATH/data/nunchuk/"
$ make -C "$LAB_PATH/../kernel/linux" M=$PWD
make: Entering directory '/home/me/embedded-linux-bbb-labs/kernel/linux'
CC [M] /home/me/embedded-linux-bbb-labs/hardware/data/nunchuk/nunchuk.o
MODPOST /home/me/embedded-linux-bbb-labs/hardware/data/nunchuk/Module.symvers
CC [M] /home/me/embedded-linux-bbb-labs/hardware/data/nunchuk/nunchuk.mod.o
LD [M] /home/me/embedded-linux-bbb-labs/hardware/data/nunchuk/nunchuk.ko
make: Leaving directory '/home/me/embedded-linux-bbb-labs/kernel/linux'
Here are a few explanations:
-
The
-C
option lets make know whichMakefile
to use, here the toplevelMakefile
in the kernel sources. -
M=$PWD
tells the kernelMakefile
to build external modules from the files in the current directory.
Now, you can install the compiled module in the NFS root filesystem by passing the modules_install
target and specifying the target directory through the INSTALL_MOD_PATH
variable.
$ make -C "$LAB_PATH/../kernel/linux" M=$PWD modules_install
make: Entering directory '/home/me/embedded-linux-bbb-labs/kernel/linux'
INSTALL /home/me/embedded-linux-bbb-labs/hardware/../tinysystem/nfsroot/lib/modules/5.15.104/extra/nunchuk.ko
DEPMOD /home/me/embedded-linux-bbb-labs/hardware/../tinysystem/nfsroot/lib/modules/5.15.104
make: Leaving directory '/home/me/embedded-linux-bbb-labs/kernel/linux'
You can see that this installs out-of-tree kernel modules under lib/modules/<version>/extra/
.
Back on the target, you can now check that your custom module can be loaded:
See kbuild/modules
in kernel documentation for details about building out-of-tree kernel modules.
However, run i2cdetect -r 1
again. You will see that the Nunchuk is still detected, but still not driven by the kernel. Otherwise, it would be signaled by the UU
placeholder.
# i2cdetect -y -r 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- 52 -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
You may also look at the nunchuk.c
file and notice a Nunchuk device probed successfully message that you didn't see when loading the module.
That's because the Linux kernel doesn't know about the Nunchuk device yet, even though the driver for this kind of devices is already loaded. Our device also has to be described in the Device Tree.
You can confirm this by having a look at the contents of the /sys/bus/i2c/
directory.
It contains two subdirectories: devices
and drivers
.
In drivers
, there should be a nunchuk
subdirectory, but no symbolic link to a device yet.
In devices
you should see some devices, but not the Nunchuk one yet.
# ls -1 /sys/bus/i2c/
devices
drivers
drivers_autoprobe
drivers_probe
uevent
# ls -1 /sys/bus/i2c/drivers/
dummy
lp872x
lp873x
lp87565
menelaus
nunchuk
palmas
pcf857x
tps65023
tps65217
tps65218
tps65910
twl
twl6040
# ls -1 /sys/bus/i2c/devices/
0-0024
0-0050
0-0070
2-0054
2-0055
2-0056
2-0057
i2c-0
i2c-1
i2c-2
19.11 Declaring I2C device
To allow the kernel to manage our Nunchuk device, let's declare the device in the custom Device Tree for our board. The declaration of the I2C1 bus will then look as follows:
&i2c1 {
pinctrl-names = "default";
pinctrl-0 = <&i2c1_pins>;
status = "okay";
clock-frequency = <50000>;
nunchuk: joystick@52 {
compatible = "nintendo,nunchuk";
reg = <0x52>;
};
};
Here are a few notes:
-
The
clock-frequency
property is used to configure the bus to operate at 50 KHz.
Although the Nunchuk operates at 100 kHz, our prototyping wiring might degrade the signal, so we prefer to operate with a lower clock frequency. -
The Nunchuk device is added through a child node in the I2C controller node.
-
For the kernel to probe and drive our device, it's required that the
compatible
string matches one of the compatible strings supported by the driver. -
The
reg
property is the address of the device on the I2C bus. If it doesn't match, the driver will probe the device but won't be able to communicate with it.
Recompile your Device Tree and reboot your kernel with the new binary.
$ cd "$LAB_PATH/../kernel/linux/"
$ make dtbs
DTC arch/arm/boot/dts/am335x-boneblack-custom.dtb
$ cp arch/arm/boot/dts/am335x-boneblack-custom.dtb /srv/tftp/
You can now load your module again, and this time, you should see that the nunchuk
driver probed the Nunchuk device:
# modprobe -r nunchuk
# modprobe nunchuk
nunchuk: loading out-of-tree module taints kernel.
input: Wii Nunchuk as /devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@0/4802a000.target-module/4802a000.i2c/i2c-1/1-0052/input/input0
Nunchuk device probed successfully
List the contents of /sys/bus/i2c/drivers/nunchuk
once again. You should now see a symbolic link corresponding to our new device.
# ls -l /sys/bus/i2c/drivers/nunchuk/
--w------- 1 4096 Jan 1 00:17 bind
lrwxrwxrwx 1 0 Jan 1 00:17 module -> ../../../../module/nunchuk
--w------- 1 4096 Jan 1 00:17 uevent
--w------- 1 4096 Jan 1 00:17 unbind
Also list /sys/bus/i2c/devices/
again. You should now see the Nunchuk device, which can be recognized through its 0052
address.
Follow the symbolic link and you should see a symbolic link back to the Nunchuk driver!
# ls -l /sys/bus/i2c/devices/
lrwxrwxrwx 1 0 Jan 1 00:14 0-0024 -> ../../../devices/platform/ocp/44c00000.interconnect/44c00000.interconnect:segment@200000/44e0b000.target-module/44e0b000.i2c/i2c-0/0-0024
lrwxrwxrwx 1 0 Jan 1 00:14 0-0050 -> ../../../devices/platform/ocp/44c00000.interconnect/44c00000.interconnect:segment@200000/44e0b000.target-module/44e0b000.i2c/i2c-0/0-0050
lrwxrwxrwx 1 0 Jan 1 00:14 0-0070 -> ../../../devices/platform/ocp/44c00000.interconnect/44c00000.interconnect:segment@200000/44e0b000.target-module/44e0b000.i2c/i2c-0/0-0070
lrwxrwxrwx 1 0 Jan 1 00:14 1-0052 -> ../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@0/4802a000.target-module/4802a000.i2c/i2c-1/1-0052
lrwxrwxrwx 1 0 Jan 1 00:14 2-0054 -> ../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@100000/4819c000.target-module/4819c000.i2c/i2c-2/2-0054
lrwxrwxrwx 1 0 Jan 1 00:14 2-0055 -> ../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@100000/4819c000.target-module/4819c000.i2c/i2c-2/2-0055
lrwxrwxrwx 1 0 Jan 1 00:14 2-0056 -> ../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@100000/4819c000.target-module/4819c000.i2c/i2c-2/2-0056
lrwxrwxrwx 1 0 Jan 1 00:14 2-0057 -> ../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@100000/4819c000.target-module/4819c000.i2c/i2c-2/2-0057
lrwxrwxrwx 1 0 Jan 1 00:14 i2c-0 -> ../../../devices/platform/ocp/44c00000.interconnect/44c00000.interconnect:segment@200000/44e0b000.target-module/44e0b000.i2c/i2c-0
lrwxrwxrwx 1 0 Jan 1 00:00 i2c-1 -> ../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@0/4802a000.target-module/4802a000.i2c/i2c-1
lrwxrwxrwx 1 0 Jan 1 00:14 i2c-2 -> ../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@100000/4819c000.target-module/4819c000.i2c/i2c-2
We are not ready to use this input device yet, but at least we can test that we get bytes when buttons or the joypad are used.
In the below command, use the same number as in the message you got in the console (event0
for input0
for example):
# od -x /dev/input/event0
0000000 02dd 0000 913f 0008 0001 0135 0001 0000
0000020 02dd 0000 913f 0008 0000 0000 0000 0000
0000040 02dd 0000 17bb 000a 0001 0135 0000 0000
0000060 02dd 0000 17bb 000a 0000 0000 0000 0000
We will use the Nunchuk to control audio playback in an upcoming lab.
19.12 Setting board model name
Modify the custom Device Tree file one last time to override the model name for your system.
Set the model
property to BeagleBone Black media player
.
$ cd "$LAB_PATH/../kernel/linux/"
$ grep "model" arch/arm/boot/dts/am335x-boneblack*.dts
arch/arm/boot/dts/am335x-boneblack.dts: model = "TI AM335x BeagleBone Black";
arch/arm/boot/dts/am335x-boneblack-wireless.dts: model = "TI AM335x BeagleBone Black Wireless";
$ less arch/arm/boot/dts/am335x-boneblack.dts
...
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
};
...
$ nano arch/arm/boot/dts/am335x-boneblack-custom.dts
$ cat arch/arm/boot/dts/am335x-boneblack-custom.dts
...
/ {
model = "BeagleBone Black media player";
};
...
$ make dtbs
DTC arch/arm/boot/dts/am335x-boneblack-custom.dtb
$ cp arch/arm/boot/dts/am335x-boneblack-custom.dtb /srv/tftp/
Recompile the device tree, and reboot the board with it. You should see the new model name in two different places:
-
In the first kernel messages on the serial console.
-
In
/sys/firmware/devicetree/base/model
. This can be handy for a distribution to identify the device it's running on.
By the way, you can explore/sys/firmware/devicetree/
and find that every subdirectory corresponds to a DT node, and every file corresponds to a DT property.
19.13 Committing kernel tree changes
Now that our changes to the kernel sources are over, create a branch for your changes and create a patch for them.
Please don't skip this step as we need it for the next labs.
First, if not done yet, you should set your identity and e-mail address in git:
This is necessary to create a commit with the git commit -s
command, as required by the Linux kernel contribution guidelines.
Let’s create the branch and the patch now:
$ git status
On branch embedded-linux-bbb
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: arch/arm/boot/dts/Makefile
Untracked files:
(use "git add <file>..." to include in what will be committed)
arch/arm/boot/dts/am335x-boneblack-custom.dts
no changes added to commit (use "git add" and/or "git commit -a")
$ git add arch/arm/boot/dts/am335x-boneblack-custom.dts
$ git commit -as -m "Custom DTS for Bootlin lab"
[embedded-linux-bbb bb7e1269527c] Custom DTS for Bootlin lab
2 files changed, 28 insertions(+)
create mode 100644 arch/arm/boot/dts/am335x-boneblack-custom.dts
$ label="v5.15.104"
$ git tag "$label-nunchuk"
We can now create the patch with git format-patch
.
This should generate a 0001-Custom-DTS-for-Bootlin-lab.patch
file.
$ label="v5.15.104"
$ git format-patch $label
0001-Custom-DTS-for-Bootlin-lab.patch
$ cp "0001-Custom-DTS-for-Bootlin-lab.patch" $LAB_PATH
Creating the branch will impact the versions of the kernel and the modules.
Compile your kernel and install your modules again and see the version changes through the new base directory for modules.
To save space for the next lab, remove the old directory under lib/modules/
containing the ”dirty” modules.
Don’t forget to update the kernel your board boots.
$ cd "$LAB_PATH/../kernel/linux/"
$ export INSTALL_MOD_PATH="$LAB_PATH/../tinysystem/nfsroot"
$ make
$ make modules
$ make modules_install
$ cp arch/arm/boot/zImage /srv/tftp/zImage
$ cd "$LAB_PATH/../tinysystem/nfsroot/lib/modules/"
$ ls -1
5.15.104-00001-gbb7e1269527c
5.15.104-dirty
$ rm -rf "5.15.104-dirty/"
$ cd "$LAB_PATH/data/nunchuk/"
$ make -C "$LAB_PATH/../kernel/linux" M=$PWD
$ make -C "$LAB_PATH/../kernel/linux" M=$PWD modules_install
Reboot your board and make sure everything's alright!
# uname -r
5.15.104-00001-gbb7e1269527c
# modprobe nunchuk
nunchuk: loading out-of-tree module taints kernel.
input: Wii Nunchuk as /devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@0/4802a000.target-module/4802a000.i2c/i2c-1/1-0052/input/input0
Nunchuk device probed successfully
19.14 Backup and restore
$ cd "$LAB_PATH/../tinysystem/nfsroot/"
$ find . -depth -print0 | cpio -ocv0 | xz > "$LAB_PATH/nfsroot-hardware.cpio.xz"
$ cd /srv/tftp/
$ tar cfJv "$LAB_PATH/hardware-tftp.tar.xz" zImage am335x-boneblack-custom.dtb
19.14.1 git bundle
To create a git bundle with just our patch (to have consistent git commit naming):
$ cd "$LAB_PATH/../kernel/linux/"
$ label="v5.15.104"
$ bundle="$LAB_PATH/kernel-linux-$label-nunchuk.bundle"
$ git bundle create $bundle $label..
To restore the git bundle:
$ cd "$LAB_PATH/../kernel/linux/"
$ label="v5.15.104"
$ bundle="$LAB_PATH/kernel-linux-$label-nunchuk.bundle"
$ git bundle verify $bundle
The bundle contains this ref:
bb7e1269527c6b56c1c5b15b1b1a5c05a2f114f2 HEAD
The bundle requires this ref:
115472395b0a9ea522ba0e106d6dfd7a73df8ba6
/home/me/embedded-linux-bbb-labs/hardware/kernel-linux-v5.15.104-nunchuk.bundle is okay
$ git checkout -b embedded-linux-bbb $label
...
$ git bundle list-heads $bundle
bb7e1269527c6b56c1c5b15b1b1a5c05a2f114f2 HEAD
$ git pull $bundle
19.15 Licensing
This document is an extension to: Embedded Linux System Development - Practical Labs - BeagleBone Black Variant
— © 2004-2023, Bootlin https://bootlin.com/, CC-BY-SA-3.0
license.