Enlaces de recursos
- Documentación online de QEMU: http://wiki.100ask.org/Qemu
- Recursos de disco de red para QEMU: http://weidongshan.gitee.io/informationdownloadcenter/boards/Sim/qemu/index.html
- Imágenes Git de QEMU: Ubuntu 18.04 y Ubuntu 16.04
- Repositorios Git del kernel y toolchain de QEMU: se gestionan con la herramienta
repo.
book@100ask:~$ git clone https://e.coding.net/codebug8/repo.git
book@100ask:~$ mkdir -p 100ask_imx6ull-qemu && cd 100ask_imx6ull-qemu
book@100ask:~/100ask_imx6ull-qemu$ ../repo/repo init -u https://e.coding.net/weidongshan/manifests.git -b linux-sdk -m imx6ull/100ask-imx6ull_qemu_release_v1.0.xml --no-repo-verify
book@100ask:~/100ask_imx6ull-qemu$ ../repo/repo sync -j2
Si todo funciona correctamente, se creará un directorio 100ask_imx6ull-qemu en /home/book.
Comandos básicos
Formato: comando [opciones] [argumentos]. Ejemplo: ls -l /home
pwd: Muestra la ruta actual.ls: Lista archivos.cd: Cambia de directorio.cd ..retrocede un nivel.mkdir: Crea un nuevo directorio.touch: Crea un archivo vacío.cp test1.txt test2.txt: Copia el archivo1 en archivo2.gedit: Editor de texto gráfico.cat: Muestra el contenido de un archivo en la terminal.rm: Elimina archivos.rmdir: Elimina directorios vacíos.clear: Limpia la pantalla de la terminal.man + comando: Muestra el manual de ayuda.mv: Renombra o mueve archivos/directorios.
Atajos de directorio:
cd ~: Va al directorio home.cd .: Directorio actual.cd ..: Directorio padre.cd ../..: Sube dos niveles.
El intérprete Shell
Shell es una aplicación que recibe comandos desde el teclado o puerto serie, los analiza y ejecuta los programas correspondientes. Busca los ejecutables en las rutas definidas por la variable de entorno PATH.
Cómo modificar PATH
- Cambio permanente (método 1): Editar
/etc/environmentcon privilegios de superusuario y añadir la ruta deseada separada por dos puntos (:). - Cambio permanente (método 2): Usar el comando
export PATH=$PATH:/ruta/deseadaen el archivo de configuración del shell (ej.~/.bashrc). - Cambio temporal: Ejecutar el comando
exportdirectamente en la terminal; solo afecta a la sesión actual.
Rutas
- Ruta absoluta: Ejemplo:
/home/solo - Ruta relativa: Desde la ubicación actual. Ejemplo: desde
/bin,./pwdejecuta/bin/pwd.cd ../videodesde/home/solo/aaalleva a/home/solo/video.
Editor vi
Configuración básica
cd /etc/vim
cp vimrc ~/.vimrc
cd ~
gedit .vimrc
Añadir al final del archivo .vimrc:
set nocompatible
set number
set backspace=2
set tabstop=4
set shiftwidth=4
set ignorecase
set hlsearch
Modos de operación
- Modo normal: Navegación, copiar, pegar, eliminar. Teclas
i,a,opara entrar en modo inserción. - Modo inserción: Edición de texto.
- Modo comandos: Para buscar (
/) o guardar/salir (:wq,:q!).
Comandos avanzados
find /ruta -name "archivo.txt": Busca archivos.grep -rn "cadena" archivo: Busca cadenas de texto en archivos.file archivo: Identifica el tipo de archivo.which comandoywhereis comando: Localizan la ubicación de un comando.- Compresión:
gzip,bzip2para un solo archivo;tarpara múltiples archivos.
Configuración de red NAT
- Configurar Ubuntu en modo NAT en VMware.
- Obtener la IP de Ubuntu.
- En Windows, crear una conexión de red virtual con una IP en el mismo segmento que Ubuntu.
- Usar Mobaxterm para conectar con Ubuntu.
Para que la placa de desarrollo acceda a Ubuntu vía NFS en modo NAT, se debe modificar el puerto mountd de Ubuntu:
sudo gedit /etc/services
# Añadir las líneas:
mountd 9999/tcp
mountd 9999/udp
Reiniciar el servicio NFS y verificar los puertos:
sudo /etc/init.d/nfs-kernel-server restart
sudo rpcinfo -p
El protocolo NFS permite que la placa acceda a un directorio de Ubuntu como si fuera local, ideal para depuración rápida de aplicaciones.
Montaje en modo NAT (ejemplo con IP de Windows 192.168.1.17):
mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.168.1.17:/home/book/nfs_rootfs /mnt
Montaje en QEMU (la IP de Ubuntu es 10.0.2.2):
mount -t nfs -o nolock,vers=3 10.0.2.2:/home/book/nfs_rootfs /mnt
Preparación para programar
Para compilar para la placa de desarrollo se necesita una cadana de herramientas de compilación cruzada. Ejemplo de compilación para la placa:
arm-linux-gnueabihf-gcc -o hello hello.c
Configuración de la cadena de herramientas
Se recomienda la configuración permanente en ~/.bashrc:
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/ruta/a/toolchain/bin
Para activar los cambios: source ~/.bashrc.
Verificación de la instalación
echo $ARCH
echo $CROSS_COMPILE
arm-buildroot-linux-gnueabihf-gcc -v
Primera aplicación en la placa
- Compilar el programa con la cadena cruzada:
arm-linux-gnueabihf-gcc -o hello hello.c. - Copiar el ejecutable al directorio NFS compartido:
cp hello /home/book/nfs_rootfs. - En la placa, montar el NFS y copiar el archivo:
cp /mnt/hello ./hello. - Ejecutar:
chmod +x hello && ./hello.
Primer controlador (driver)
Compilar el kernel
cd Linux-5.4
make 100ask_stm32mp157_pro_defconfig
make uImage LOADADDR=0xC2000040 -j4
make dtbs
cp arch/arm/boot/uImage ~/nfs_rootfs
cp arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb ~/nfs_rootfs
Compilar e instalar módulos del kernel
make modules -j4
sudo make INSTALL_MOD_PATH=/home/book/nfs_rootfs modules_install
Reemplazar kernel y módulos en la placa
Montar NFS y copiar los archivos necesarios:
mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.168.1.100:/home/book/nfs_rootfs /mnt
cp /mnt/uImage /boot
cp /mnt/*.dtb /boot
cp /mnt/lib/modules /lib -rfd
sync
reboot
Probar el driver hello_drv
- Modificar el
Makefilepara queKERN_DIRapunte al directorio del kernel. - Compilar con
make. - Copiar
hello_drv.koyhello_drv_testa la placa. - Cargar el driver:
insmod hello_drv.ko. - Ejecutar el programa de prueba:
./hello_drv_test -w www.100ask.net && ./hello_drv_test -r.
Números de inodo
Cada archivo tiene un número de inodo que almacena sus metadatos.
Enlace duro
Similar a una copia, pero sin ocupar espacio adicional. Ambos archivos comparten el mismo inodo. Solo funciona dentro del mismo sistema de archivos y no admite directorios.
ln archivo1.c archivo2.c # Crea un enlace duro
Enlace simbólico (blando)
Similar a un acceso directo. Tiene su propio inodo y no incrementa el contador de enlaces. Si se elimina el archivo original, el enlace queda roto.
ln -s /ruta/absoluta/archivo1.c enlace.c
Colores de archivos en Ubuntu
Compilación
Un archivo C/C++ pasa por cuatro etapas: preprocesamiento, compilación, ensamblado y enlace.
- Compilar todo junto:
gcc -o test main.c sub.c - Compilar por separado y enlazar:
gcc -c -o main.o main.c; gcc -c -o sub.o sub.c; gcc -o test main.o sub.o
Uso de Makefile
Un Makefile contiene reglas con el formato:
objetivo: dependencias
<tabulador>comandos
Los comandos se ejecutan si las dependencias son más recientes que el objetivo o si el objetivo no existe.
Funciones útiles en Makefile
$(foreach var,list,text): Itera sobre una lista.$(wildcard pattern): Expande patrones de archivos existentes.
Mejora progresiva del Makefile
Versión 1: Simple pero ineficiente.
test : main.c sub.c sub.h
gcc -o test main.c sub.c
Versión 2: Eficiente pero verbosa y sin detección de cambios en cabeceras.
test : main.o sub.o
gcc -o test main.o sub.o
main.o : main.c
gcc -c -o main.o main.c
sub.o : sub.c
gcc -c -o sub.o sub.c
clean:
rm *.o test -f
Versión 3: Más concisa, pero sin detección de cabeceras.
test : main.o sub.o
gcc -o test main.o sub.o
%.o : %.c
gcc -c -o $@ $<
clean:
rm *.o test -f
Versión 4: Con soporte manual para cabeceras.
test : main.o sub.o
gcc -o test main.o sub.o
%.o : %.c
gcc -c -o $@ $<
sub.o : sub.h
clean:
rm *.o test -f
Versión 5: Con detección automática de dependencias de cabeceras.
objs := main.o sub.o
test : $(objs)
gcc -o test $^
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
%.o : %.c
gcc -Wp,-MD,.$@.d -c -o $@ $<
clean:
rm *.o test -f
distclean:
rm $(dep_files) *.o test -f
Variables en Makefile
A = xxx: Varible de asignación diferida (se evalúa al usarla).B ?= xxx: Asignación condicional (solo si no está definida).C := xxx: Variable de asignación inmediata.D += yyy: Asignación adicional (hereda el tipo de la variable D).
Se pueden exportar variables con export para que estén disponibles en sub-Makefiles. Ejemplo: export CC = $(CROSS_COMPILE)gcc.
Objetivos ficticios (.PHONY)
.PHONY: clean
clean:
rm *.o test -f
Entrada/Salida de archivos (File I/O)
En Linux, todo es un archivo. Las operaciones se realizan a través de las funciones estándar de I/O.
Función open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
flags puede ser: O_RDWR, O_RDONLY, O_WRONLY, O_APPEND, O_TRUNC, O_CREAT. El parámetro mode solo es necesario si se usa O_CREAT. Devuelve un descriptor de archivo o -1 en caso de error.
Framebuffer
Principio de operación de una pantalla LCD
El controlador de Framebufffer en Linux reserva una memoria (Framebuffer) que contiene los datos de la imagen. El programa de usuario puede acceder a ella mediante mmap.
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
Se usa para interactuar con el controlador (por ejemplo, obtener la resolución de la pantalla).
Función mmap
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
Mapea un archivo o dispositivo en la memoria del proceso. Parámetros clave: prot (protección: PROT_READ, PROT_WRITE) y flags (MAP_SHARED para escritura compartida).
Ejemplo de uso del Framebuffer
int fb_fd = open("/dev/fb0", O_RDWR);
if (fb_fd < 0) { /* error */ }
struct fb_var_screeninfo var;
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &var)) { /* error */ }
int screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
unsigned char *fb_base = (unsigned char *)mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
if (fb_base == MAP_FAILED) { /* error */ }
Codificación UNICODE (UTF-8)
UTF-8 usa codificación de longitud variable. Los caracteres ASCII (0-127) se representan con un byte. Los caracteres no ASCII usan de 2 a 4 bytes, donde el primer byte indica la longitud total en sus bits más significativos.
Marco de trabajo del sistema de entrada
Estructura input_event
type: Tipo de evento (ej.EV_KEYpara teclas,EV_ABSpara posición absoluta).code: Código específico del evento (ej.KEY_Apara la tecla A).value: Valor del evento (0 para liberar, 1 para presionar en teclas; coordenadas en pantallas táctiles).- Evento de sincronización: Un evento con
type,codeyvalueigual a 0, que indica que todos los datos de una transacción han sido enviados.
Formas de acceso a hardware desde una APP
- Modo consulta (polling): Se abre el dispositivo en modo no bloqueante (
O_NONBLOCK). La funciónreadretorna inmediatamente, con datos o con error. - Modo dormir-despertar: Se abre en modo bloqueante. La función
readduerme el proceso hasta que lleguen datos. - Modo POLL/SELECT: Se usa
poll()oselect()con un tiempo de espera. Si hay datos, retornan inmediatamente; de lo contrario, duermen hasta que lleguen datos o se cumpla el tiempo de espera. - Modo de notificación asíncrona: La aplicación se registra para recibir una señal (ej.
SIGIO) cuando el dispositivo tenga datos. Pasos:- Definir un manejador de señal:
signal(SIGIO, mi_funcion); - Indicar al controlador el PID del proceso:
fcntl(fd, F_SETOWN, getpid()); - Habilitar la notificación asíncrona:
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | FASYNC);
- Definir un manejador de señal:
Desarrollo de controladores en Linux
Estructura file y file_operations
Cuando una aplicación abre un archivo, el kernel crea una estructura struct file que contiene información como los flags y la posición actual. La estructura file_operations es un vector de funciones que mapea las llamadas del sistema de la aplicación (open, read, write, close) a las funciones del controlador.
- Determinar el número de dispositivo mayor o pedir uno al kernel.
- Definir una estructura
file_operations. - Implementar las funciones necesarias (
drv_open,drv_read, etc.) y asignarlas a la estructura. - Registrar el controlador con
register_chrdeven la función de entrada (module_init). - Proveer una función de salida (
module_exit) que llame aunregister_chrdev. - Opcionalmente, crear clases y dispositivos automáticos con
class_createydevice_create.
Filosofía de diseño de controladores
- Orientado a objetos: Todos los controladores implementan
file_operations. - Separación en capas: La capa superior maneja la lógica independiente del hardware (registro del controlador), mientras que la capa inferior es específica del hardware.
- Separación izquierda-derecha: La capa inferior se divide para manejar diferentes periféricos, permitiendo elegir funciones según los recursos disponibles.
Tutorial de la placa virtual QEMU IMX6ULL
Preparación
- Instalar VMware y una imagen de Ubuntu 18.04 o 16.04.
- Configurar la máquina virtual en modo NAT.
- Instalar KVM para acelerar QEMU:
sudo apt-get install qemu qemu-kvm libvirt-bin bridge-utils virt-manager. - Clonar el repositorio de la imagen QEMU:
git clone https://e.coding.net/weidongshan/ubuntu-18.04_imx6ul_qemu_system.git.
Ejecución
./install_sdl.sh # Solo la primera vez
./qemu-imx6ull-gui.sh # Inicia la placa simulada (usuario: root, sin contraseña)
Operaciones básicas en QEMU
- LCD:
fb-testomyfb-test /dev/fb0. - LEDs: Cargar el driver y usar
ledtest /dev/100ask_led0 on. - Botones y LEDs: Cargar los drivers correspondientes y ejecutar
button_led_test. - I2C (AT24C02): Usar
i2c_usr_testpara leer y escribir en la memoria EEPROM.
Obtención del código fuente del kernel
Usar la herramienta repo como se indicó al inicio del artículo.
Compilación del kernel para QEMU
cd linux-4.9.88
make mrproper
make 100ask_imx6ull_qemu_defconfig
make zImage -j2
make dtbs
Los archivos resultantes son arch/arm/boot/zImage y arch/arm/boot/dts/100ask_imx6ull_qemu.dtb.
Reemplazo del kernel en QEMU
Copiar los archivos anteriores al directorio de la imagen QEMU (imx6ull-system-image).
Modificación del sistema de archivos
Montar la imagen rootfs.img:
sudo mount -o loop rootfs.img /mnt
# Realizar modificaciones en /mnt
sudo umount /mnt
Uso de NFS con QEMU
- Instalar y configurar el servidor NFS en Ubuntu:
sudo apt-get install nfs-kernel-server
sudo gedit /etc/exports
# Añadir: /home/book/nfs_rootfs *(rw,nohide,insecure,no_subtree_check,async,no_root_squash)
sudo /etc/init.d/nfs-kernel-server restart
- En QEMU, montar el directorio NFS:
ifconfig eth0 10.0.2.15
mount -t nfs -o nolock,vers=3 10.0.2.2:/home/book/nfs_rootfs /mnt
Nota: QEMU no hace ping a Ubuntu, pero el montaje NFS funciona correctamente.