Manual
do
Maker
.
com
Nesse post disponho um exemplo de comunicação entre uma controladora PIC e um módulo WiFi ESP8266, trocando informações com uma aplicação remota (feita em Qt) através de comunicação socket. A programação do PIC está sendo feita através da MikroC IDE, que permite fazer a programação da MCU em linguagem C, além de possuir bibliotecas para muitas funções, tal qual em Arduino.
Utilizo mais uma vez o módulo ESP8266-01, citado anteriormente nesse outro post, onde foi utilizado um buffer não inversor CD4050 para baixar a tensão de 5v para 3.3v na comunicação entre o Arduino e o ESP8266. Dessa vez não será necessário utilizar o buffer porque a microcontroladora PIC utilizada é a P16F883, que trabalha de 2v a 5.5v, então, utilizaremos diretamente 3.3v para a alimentação de ambos os componentes.
Utilizarei aqui o sensor de linha, descrito nesse post para exemplificar a coleta e emissão de dados através da comunicação via socket.
Ao final do artigo, disponibilizarei os links para download dos códigos de exemplo, portanto, sinta-se livre para desfrutar da leitura.
Escolhi essa MCU porque das pequenas que tenho é a que me oferece um recurso indispensável; UART port hardware. Do mesmo modo que com Arduino, é possível utilizar o recurso de softserial, mas em altas velocidades começa a gerar ruído e isso é uma das coisas que não deve acontecer nessa comunicação. Além disso, a MCU será configurada para utilizar o oscilador interno (que é outro recurso que considero indispensável para simplificação da prototipagem e economia de energia) em 8MHz, podendo ser ajustado até 32KHz, mas testes com a UART deverão ser executados para tal. Rodando em 32kHz a 2v, a corrente é de 11uA, mas não seria possível utilizar a UART À 38400 kbauds. É uma MCU extremamente econômica; utilizando qualquer timer que não o TMR0, pode-se fazê-la dormir e levantar em uma interrupção, porém não será necessário esse extremo. Em outros posts sobre PIC veremos como espremê-lo até ligá-lo a um limão :-)
Repare no desenho que cada pino possui diversas funcionalidades. Para utilizar uma delas especificamente, deve-se configurar a MCU previamente. O clock também é configurado por software, por isso costumo criar uma função setup() como a do Arduino para inicializar as configurações previamente (mas nada que não pudesse ser feito diretamente em main() - e assim será). Não vou colocar todo o código no post senão ficará enorme, mas vou citar as partes que acho importante.
Para iniciar, crie um projeto novo na IDE MikroC, selecionando o modelo da MCU (PIC16F883) e clock interno a 8MHz na configuração de bits. Ainda na configuração de bits, desabilite o Brown-Out, que é um fusível para reiniciar o PIC quando ele estiver abaixo de 4.5v (isso também é configurável, mas desse modo já basta). Após isso, uma função setup contendo as configurações descritas:
void setup(){
C1ON_bit = 0;
C2ON_bit = 0;
O pino analógico 0 será utilizado para fazer a leitura do sensor de linha, os demais pinos serão configurados como digital:
//Tratamento dos pinos Analogicos
ANSEL = 0x01; // RA0 como entrada analógica
ANSELH = 0; // Demais pinos, digital
Uma vez que a leitura analógica será utilizada, deve-se iniciar o módulo AD:
//Inicializacao do modulo AD
ADC_Init();
Em PIC define-se em TRISx se o pino será input ou output. Para fácil assimilação, pense em 1nput e 0utput. Colocando 0xFF, todos os bits são setados como 1nput no TRIS da porta A e C, como exemplificado abaixo:
//TRIS
TRISA = 0xFF; //TUDO INPUT nas portas A
TRISC = 0xFF; //o mesmo para portas C
A interrupção será habilitada apenas para fazer a leitura de RX. Há como fazê-lo sem interrupção, uma vez que todo o código seguirá um fluxo e nesse caso, a interrupção poderia ser dispensada. Mas é válida como exemplo porque poderia haver um caso de interrupção externa gerada por um sensor digital, por exemplo. Assim que tiver uma oportunidade, escrevo algo a respeito, mas inicialmente um bom exemplo sobre interrupções pode ser visto aqui, ou aqui, também aqui, mais um aqui e um que gosto bastante, bem aqui.
Basicamente, será habilitada a interrupção externa no RX, desabilitada no TX e periféricamente. Por fim, a chave geral das interrupções é ligada.
//interrupcoes
// RCIE___./ .___PEIE___./ .___GIE
RCIE_bit = 1; // habilitar interrupcao em RX
TXIE_bit = 0; // desabilita interrupcao em TX (default)
PEIE_bit = 1; // habilitar/desabilitar interrupcoes perifericas
GIE_bit = 1; // liga chave geral das interrupcoes
TMR0IE_bit = 1; //interrupcao do timer0
As configurações não são feitas "de cabeça". Obviamente eu li o datasheet para executá-las, ainda mais que programo um monte de modelos de PIC, então não quero e não vejo razão para decorar tudo, uma vez que está documentado, bastando saber o quê procurar na documentação. Para configurar o oscilador interno a 8MHz, o seguinte conjunto de bits deve ser disposto no registrador OSCCON:
//oscilador interno a 8MHz
OSCCON = 0b01110101;
Esse tunning pode ser necessário dependendo da frequência selecionada (qualquer coisa que não 8MHz). O datasheet descreve de forma sublime a configuração interna do oscilador interno.
//Tunning nao necessario porque esse PIC ja vem calibrado a 8MHz
//OSCTUNE = 0b00001111;
Poderia sem dúvida alguma ser feita no loop principal porque o código
não foi escrito para ser multithread, mas não por isso deixarei de me divertir um pouco com os recursos
da controladora. Abaixo, a configuração do timer0, descrito nesse post.
//=-=-=-=-=-=-=-=-=-= TMR0 =-=-=-=-=-=-=-=-=-=-=-=-=
/* Overflow:
Fosc = Fcy/4
Fovr = Fosc/(4*PRESCALER*RESOLUTION_TIMER(*POST))
Comparador:
Fccpif = Fosc/(4*COMPARATOR*PRE)
*/
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
/* =-=-=-=-= Calculando o valor do timer =-=-=-=-=-=
ciclo de maquina = Fosc/4 para o oscilador interno
Tempo (t) do overflow:
t = ciclo * prescaler * TMR0
O clock esta configurado para 8Mhz (8000000)
ciclo = 8/4 = 2us
prescaler = 1:2 (2)
t = 2 * 2 * 256 (TMR0 guarda de 0 a 255 antes do overflow porque ele tem 8 bits)
t = 1ms
Desse modo, a piscada eh a cada 1 segundo. Com prescaler a 1:4, dobra o tempo, logo,
blink de 1 segundo.
//RBPU 1 - pullup disabled na portb
//INTEDG 0 - detecta interrupcao em LOW
//T0CS 0 - Fosc/4 (clock interno)
//T0SE 0 - incrementa da baixa pra alta
//PSA 0 - prescaler enabled
//PS2,PS1,PS0 010 - 1:4 no prescaler do TMR0
*/
OPTION_REG = 0b10000010;
Tal qual em Arduino, devemos inicializar a UART, mas como estamos utilizando uma MCU virgem, sem bootloader nem cristal, será necessário verificar o baud rate suportado conforme o clock utilizado. O cálculo do baud rate foi exemplificado nesse outro post.
//Inicializa UART
UART1_Init(38400);
Delay_ms(100);
O ESP8266 já está previamente configurado para entrar na rede, portanto a única preocupação desse programa é garantir que seja possível trocar mensagens com o servidor através do ESP8266. Para tal, simplesmente valida-se as configurações de múltiplas conexões e a comunicação TCP estabelecida com o socket remoto. Simplesmente estou enviando os comandos AT,CIPMUX,TCPSTART e CIPSEND. Consegui deixar espaço o suficiente na memória para processar tratamento de erros e tomada de decisão, mas não vou implementar agora porque tenho vários outros códigos pra por a mão e no momento me basta que isso funcione.
Ainda que o servidor (que recebe a conexão socket) caia ou seja reiniciado, do modo burro que o programa está operando agora, automaticamente se reconectará e em algum momento dentro de 30 segundos já estará devidamente sincronizado com a aplicação. Criei uma função que chama outras, sendo essa a rotina:
void sendSample(){
//1 - esp8266 responde?
atStatus();
//Forca cipmux, nao importa se ja esta
cipmux();
//estabelece a conexao TCP remota
tcpStart();
//envia a mensagem
sender();
//envia sem verificao, como todas funcoes anteriores
sensorStatus();
}
Voltando ao início do código, algumas definições foram criadas, relacionadas às configurações de rede. O ID do dispositivo, IP do servidor, porta de conexão remota e ID da conexão TCP com o ESP8266.
A diretiva DEFINE não ocupará memória do programa, ele é tratado previamente à compilação. Essa diretiva é útil para quando se quer ter uma visão clara de uma função ou para não ter que decorar valores, ou para digitar pouco, etc.
//PIC16F690
//=-=-=-=-=-= DEFINES =-=-=-=-=-=
#define ON 0
#define OFF 1
#define MSGLEN 19
#define OK 1
#define DONE 0
#define MYID "1"
#define TCPID "4"
#define SERVER "192.168.1.232"
#define PORT "9000"
#define PROTOCOL "TCP"
#define LOW 0
#define HIGH 1
#define TIMEOUT 10
#define YES 1
#define NO 0
#define UDELAY 20
#define DELAY 100
#define UART_DELAY 400
#define CIPSIZE 3
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Diversas variáveis estão acessíveis de forma global:
//=-=-=-=-=-=-=-=-=-=-=-= int =-=-=-=-=-=-=-=-=-=-=-=-=-=//
//contador de uso geral //
int count = 0; //
//acumulador de overflows do timer0, ate 1seg (1000ms). //
int oneSecond = 0; //
//media de 3 amostragens do sensor //
int sensorSample = 0; //
int rxRead = 0; //
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=//
//-=-=-=-=-=-=-=-=-=-=-=-= char =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=//
// 19 bytes. triste, mas inevitavel por causa da mensagem de conexao TCP //
char text[MSGLEN] = {0}; //
//tamanho da mensagem de envio. Ex.: esp8266:15,1 //
char cipsendSize[CIPSIZE] = {0}; //
//acumulador de segundos. trabalha em conjunto com o oneSecond //
char secs = 0; //
//guarda a palavra a encontrar na resposta do reader() //
char word = 0; //
//guarda o valor do sensor //
char sensorVal = 0; //
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=//
Costumo fazer um rascunho da MCU utilizada para me guiar durante a codificação:
//=-=-=-=-=-=-= PIC16F690 =-=-=-=-=-=-=-=
/*
//mapeamento dos pinos utilizados
//=-=-=-=-=-=-= PIC16F883 =-=-=-=-=-=-=-=
/*
-|1 U 28|-
-|2 27|- RB6 READER (flag)
-|3 26|- RB5 (Sensor - Analogico 13)
-|4 25|-
-|5 24|-
-|6 23|-
-|7 22|-
VSS -|8 21|-
-|9 20|- VDD
-|10 19|- VSS
RESET RC0 -|11 18|- RC7 (RX)
RC1 -|12 17|- RC6 (TX)
DBG RC2 -|13 16|-
LED RC3 -|14 15|- RC4
*/
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Aliases (ou "apelidos") para as portas e pinos são bastante úteis para não ter que decorar os nomes dos respectivos bits, assim como são facilitadores para trocar os pinos em questão, bastando alterá-lo na definição do alias e não sendo necessário tocar no restante do código.
//=-=-=-=-=-=-=-= ALIASES =-=-=-=-=-=-=-=
sbit RX_port at RC7_bit; //RX
sbit TX_port at RC6_bit; //TX
//TESTE de delay, conexao etc.
sbit TRIS_LED at TRISC3_bit;
sbit LED_port at RC3_bit;
//debugger temporario
sbit TRIS_DBG at TRISC2_bit;
sbit DBG_port at RC2_bit;
// hard reset
sbit TRIS_RESET at TRISC0_bit;
sbit RESET_port at RC0_bit;
//flag para leitura da uart
sbit TRIS_READER at TRISB6_bit;
sbit READER_port at RB6_bit;
//SENSOR ANALOGICO
sbit TRIS_SENSOR at TRISB5_bit;
sbit SENSOR_port at RB5_bit;
//UART
sbit TRIS_RX at TRISC7_bit;
sbit TRIS_TX at TRISC6_bit;
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Na função main(), alguns ajustes mais relacionados aos TRIS e PORT podem ser vistos em detalhes baixando o código nesse link.
Assim, estão finalizadas as configurações iniciais. Agora podemos partir para o código que fará a leitura do sensor e o envio através do ESP8266.
Limpar variáveis
Pode parecer preciosismo e talvez até seja, mas deixe-me iniciar com minhas considerações:
Pode-se então utilizar strncpy em conjunção a um outro recurso da linguagem e preencher o buffer, mas não existe mágica, de alguma forma haverá uma interação de uma dessas funções junto com a sua. Como eu sei o tamanho do array, prefiro ir diretamente ao ponto, fazendo um loop e preenchendo o array de char com 0 (porque '\0' é o 0 literal). Por isso crio essa função sempre:
void clear(char *var,short int size){
for (i=0;i<size;i++){
var[i] = 0;
}
}
A primeira etapa da comunicação entre a MCU e o ESP8266 é o status serial através do comando 'AT', sem parâmetros. Observe que o ESP826 espera CR (carriege return) e LF (Line Feed), portanto devemos somar os bytes da mensagem excluindo "rn", mas não deixando de enviá-los.
Como citei anteriormente, fiz questão de colocar pra funcionar primeiro e os tratamentos de exceções serão feitos posteriormente, mas essa função é a primeira necessária para a validação.
void atStatus(){
word = 'O';
//A funcao reader() se encarrega de fazer o tratamento da resposta
write("ATrn");
}
A função write() é um auxiliador na escrita, nada muito significativo. A variável word é uma flag; 'O' quer dizer que na resposta deverá ser esperado por "OK". Mais uma vez - não está sendo tratado ainda nessa versão do programa.
A resposta ao comando AT é tratada pela função reader(), invocada através da flag em interrupts(), e verificada posteriormente na função main(). Nesse ponto, existe uma possibilidade de falha; o que acontece se a rede não responder? Bem, isso não está sendo tratado agora e utilizar um timer para temporizar a leitura seria interessante. Ainda há a possibilidade de haver resposta, mas ser diferente da esperada. Como tratar um erro de resposta do comando AT? A única coisa que me ocorre no momento é um reset():
void hardReset(){
RESET_port = LOW;
Delay_ms(50);
RESET_port = HIGH;
}
Após o reset (convenhamos que fazê-lo no pino é menos trabalhoso), em dado momento o envio de comandos AT por parte do PIC voltará a ser efetivo, por isso considerei rodar essa versão do programa sem tratamento de exceções.
O próximo passo que tomei foi habilitar múltiplas conexões. Em uma chamada cíclica da função sendSample() todas as rotinas de comunicação se repetem, porém o único comando dessa rotina que é aplicado é o cipsend. Mas não importa, porque não causa erros de funcionamento e garante que em caso de reset de qualquer ponto, tudo volte a funcionar sem tratar logicamente qualquer condição.
void cipmux(){
word = 'O';
//libera a mensagem para a funcao write()
write("AT+CIPMUX=1rn");
Delay_ms(UART_DELAY);
}
A função de conexão TCP é a próxima da lista:
void tcpStart(){
word = 'L';
//garantir que o array de msg esta limpo, apesar de nao usado.
/* A mensagem eh grande demais. Para compor uma string com o P16F883
seria necessario manipular o banco de memoria atraves do bit IRP, mas
nao seria uma tarefa trivial. Como o ESP8266 espera por rn (CF,LF ou
para os puristas, 0x0D 0x0A), da pra ir escrevendo chuncks.
*/
clear(text,MSGLEN);
//reservar memoria pra fazer apenas 1 write nao daria porque o PIC16F883
//reclama da alocacao de memoria. Assim resolve-se o problema e ainda
//foram economizados muitos bytes.
write("AT+CIPSTART=");
Delay_ms(UDELAY);
//strcat(text,TCPID);
write(TCPID);
Delay_ms(UDELAY);
//strcat(text,',');
write(",");
Delay_ms(UDELAY);
//strcat(text,""TCP",");
write(""TCP",");
Delay_ms(UDELAY);
//strcat(text,""");
write(""");
Delay_ms(UDELAY);
//strcat(text,SERVER);
write(SERVER);
Delay_ms(UDELAY);
//strcat(text,"",");
write("",");
Delay_ms(UDELAY);
//strcat(text,PORT);
write(PORT);
Delay_ms(UDELAY);
//strcat(text,"rn");
write("rn");
Delay_ms(UART_DELAY);
}
Apesar de não ser complexo, o envio de uma mensagem TCP é dividida em duas etapas; primeiro informa-se ao ESP8266 o ID da conexão a utilizar seguido do comprimento (em bytes) da mensagem a ser enviada - lembrando que CF/LF não devem ser contabilizados. Aguarda-se então a resposta e abrir-se-á um pseudo-prompt ">". Nesse momento a mensagem já pode ser enviada. MAS, e tão somente "MAS", eu mandei tudo sem validação, como já citei 4 outras vezes.
void sender(){
sensorVal = getSensorValue();
//clear usa count no loop. como a var eh reaproveitada, faz-se o clear
//primeiro e depois calcula-se o tamanho de alocacao da msg
clear(text,MSGLEN);
//1 - aloca o tamanho da mensagem a enviar
//10 = esp8266:X, ; -2 = rn ; final = esp8266:X,yrn onde X pode ter 2 bytes
count = 10 + strlen(MYID); //rn nao entra na conta
/* IntToStr precisa de no minimo 7 bytes, por isso eh melhor utilizar o array
text, depois copiar o resultado para cipsendSize, que soh ocupa 2 bytes.
O lado negativo eh que um loop foi necessario.
*/
IntToStr(count,text); //- - - - - - - - - - - - _ '1' '�'
clear(cipsendSize,CIPSIZE);
for (count=4;count<7;count++){ //0 1 2 3 [4] [5] [6] //se a posicao nao for em branco e nem terminador nulo... if (text[count] != ' ' && text[count] != 0){ if (cipsendSize[0] == 0){ cipsendSize[0] = text[count]; } else{ cipsendSize[1] = text[count]; break; } } } //limpa as variaveis utilizadas (count eh limpo pela funcao clear()) clear(text,MSGLEN); /*A mensagem a enviar eh dividida em 2 etapas: CIPSEND: Indica a conexao (pelo ID) e o tamanho da mensagem que sera enviada. Apos enviar essa mensagem, um prompt deve ser recebido na leitura. A interrupcao devera tratar a resposta, aguardando por um '>', entao a segunda fase eh o envio
da mensagem, do tamanho informado nesse comando. O envio de '>' pelo ESP8266
eh tratado na interrupcao, que informa ao reader() que tem dados. A funcao
reader() le e se encontra '>', executa sensorStatus().
*/
strcpy(text,"AT+CIPSEND="); //formata a primeira mensagem
strcat(text,TCPID); //escolha da conexao a utilizar (tcp 4)
strcat(text,","); //...
strcat(text,cipsendSize); //... e concatena o tamanho da msg TCP
strcat(text,"rn");
clear(cipsendSize,CIPSIZE);
word = '>';
count = 0; //pena ter que fazer isso aqui, mas o contador precisa estar limpo
//na interrupcao do reader
write(text);
Delay_ms(UART_DELAY);
}
Ficou convencionado o formato de mensagem "esp8266:id,status" (porque a definição é minha e eu escrevi tudo sozinho, então eu quero que seja assim) :-)
A função reader() é quem terá um pouco mais de inteligência, uma vez que deve tratar as respostas da interrupção, mas agora está um tanto quanto imprestável:
void reader(){
READER_port = LOW;
if (UART1_Data_Ready()){
text[count] = UART1_Read(); //le o byte na posicao text[posicao]
//mesmo que vier rn nao tem erro pq text tem espaco. O prompt abre na mesma linha, nao tem terminador
if (text[count] == '>'){
sensorStatus();
DBG_port = !DBG_port; //TODO: remover o led de debug (?)
clear(text,MSGLEN);
}
else if (text[count] == 'n'){
text[count+1] = '�';
//se casar, limpa as variaveis
if (word == 'O'){
if (strcmp(text,"OKrn") == 0){ //encontrou!
//DEBUG no LED - TODO: remover
DBG_port = !DBG_port;
//se for r ou n, a linha acabou de qualquer modo
clear(text,MSGLEN);
}
}
//esse if eh uma repeticao estupida, mas por causa da memoria.
//do modo 'clean' estava ocupando 194 bytes, agora esta com
//173 bytes de RAM em uso.
else if (word == 'L'){
text[count+1] = '�';
if (strcmp(text,"Linkedrn") == 0){ //encontrou!
//DEBUG no LED
DBG_port = !DBG_port;
//se for r ou n, a linha acabou de qualquer modo
clear(text,MSGLEN);
}
}
}
count++;
}
}
A interrupção não mantém código, mas existe um conjunto de regras a obedecer (ver referencias anteriormente citadas):
1 - desligar a chave geral das interrupções, pois interrupção tem prioridade máxima e todo o restante do processamento é parado para recebê-la.
2 - limpar a flag da interrupção, senão ao voltar a chave geral, haverá uma interrupção imediatamente
3 - tratar a causa da interrupção (que é o esperado). Aí entra a função reader()
4 - levantar a chave geral para aguardar por novas interrupções:
void interrupt(){
//desabilita a chave geral de interrupcoes pra nao chegar mais nada
GIE_bit = 0;
//se for UART...
if (RCIF_bit == 1){ //ESSA INTERRUPCAO NAO ESTA EM USO
// Checagem de erro de frame
if(FERR_bit == YES) {
rxRead = RCREG;
GIE_bit = 1;
return;
}
// overun
if(OERR_bit == YES) {
CREN_bit = 0;
CREN_bit = 1;
GIE_bit = 1;
return;
}
//RCIF_bit nao pode ser limpo por software, tem que ser assim nessa MCU;
//lendo o RCREG para uma variavel
count = RCREG;
count = 0;
//apontando essa flag, o reader() eh executado no loop principal
READER_port = HIGH;
//zera o timer e comeca novamente
/*Se houve leitura UART, reset do timer ate que nao tenha mais mensagem
a ler, entao temporiza os 10 segundos*/
TMR0 = oneSecond = secs = 0;
}
//se for TMR0
if (TMR0IF_bit){
//zera o timer
TMR0 = 0;
TMR0IF_bit = 0; //essa eh a flag apos o overflow; limpando...
oneSecond++; //temporizador de 1 segundo (acumula os estouros de TMR0)
if(oneSecond > 999){ //prescaler 1:8 - 1000ms
secs++; //somador de segundos (limpo no loop principal)
//Apos ter acumulado um segundo de overflow, volta a 0
oneSecond=0;
}
}
//flags limpas e eventos tratados, basta levantar a chave geral novamente
GIE_bit = 1;
}
A conexão TCP é feita como exemplificado nesse post. Mas aqui tem algumas implementações importantes, principalmente relacionado à manipulação das células dessa tabela que mantém o status dos dispositivos conectados.
Estou disponiblizando o programa em Qt sem finalizar também, porque nesse momento é só a prova de conceito para um projeto, mas adianto que as celulas receberão um label baseado em arquivo, utilizando o QSettings. Desse modo, poderia ser monitoramento de portas, por exemplo; ID 1 seria porta do banheiro, então o label seria BANHEIRO, mas se trocar o sensor de lugar pra monitorar a garagem, basta trocar o valor do label ID1 no arquivo labels.ini.
O código (bem simples) em Qt está(va) disponível em um link (que sumiu do Dropbox).
Obviamente esse código será melhorado, a aplicação em Qt receberá mais recursos etc. Essa foi apenas uma prova de conceito utilizando um PIC médio. Assim que eu fiz o video com alguns sensores, atualizo esse post.
Inscreva-se em noss canal DobitAoByte no Youtube!
Autor do blog "Do bit Ao Byte / Manual do Maker".
Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.