Skip to content

Documentação

Gabriel Franceschi Libardi edited this page Dec 19, 2023 · 14 revisions

Documentação do Projeto

Bem vindo a página da documentação do projeto em Português Brasileiro 🇧🇷!

Gostaria de mencionar que este projeto foi desenvolvido para a disciplina SEL0614- Aplicação de Microprocessadores da Universidade de São Paulo, ministrada pelo Prof. Dr. Pedro Oliveira, durante o 2º Semestre de 2023.

Sumário

  • Introdução: Explicação dos conceitos utilizados e detalhes sobre o PIC18F4550;
  • Implementação: Explicação de como foi feita a implementação e seus detalhes;
  • Comparação com Implementação em Assembly 8051: Discussão sobre vantagens e desvantagens do Projeto.

Introdução

Nesta seção, abordados os conceitos chave para a compreensão do funcionamento do código.

Temporização

A temporização em microcontroladores se dá, em suma, através da regulação do tempo em que um registrador (timer) leva para transbordar (sofrer overflow). Mas, como regulamos esse tempo?

Muito simples! Como se trata de um registrador, podemos escolher em que valor iniciamos a contagem e assim regular o período entre os overflows. Assumindo um timer de 16 bits (valor máximo 0xFFFF ou 65535):

$$\Large t_{\text{overflow}} = \frac{65535 - valor_{o}}{f_{\text{interna}}}$$

em que $t_{\text{overflow}}$ é o tempo de transborda, $valor_{o}$ é o valor inicial do registrador e $f_{\text{interna}}$ é a frequência entre incrementos do registrador (definida internamente).

Contudo, e se quisermos um ajuste maior dos tempos possíveis, para uma dada frequência interna fixa? Neste caso, precisamos levar em consideração o chamado pré-escalador (prescaler). Mas o que seria isso? O pré-escalador é uma ferramenta que nos permite pré escalar uma dada frequência de referência e fornecer um novo valor frequência escalado da referência. No contexto deste projeto, o pré-escalador do PIC18F4550 é definido por software e fornece novos valores de frequência menores que a frequência de referência (permitindo assim a escolha de períodos maiores).

O período de overflow para um prescale de 1:32 seria:

$$\Large t_{\text{overflow}} = \frac{32\cdot(65535 - valor_{o})}{f_{\text{interna}}}.$$

Considerando que, para o PIC18F4550, a frequência usada no timer é sempre $1/4$ da frequência do cristal oscilador $f_{\text{osc}}$, a fórmula completa seria:

$$\Large t_{\text{overflow}} = \frac{4\cdot32\cdot(65535 - valor_{o})}{f_{\text{osc}}}.$$

Por fim, como descobrimos o valor inicial? Neste caso, assumindo um cristal de $8\hspace{0.1cm}\text{MHz}$ e um $t_{\text{overflow}}$ de 1s, teríamos:

$$\Large valor_{o} = 65535 - \frac{8\cdot10^{6}}{32\cdot 4} = 3035$$

que em hexadecimal corresponde a 0x0BDB!

Interrupções no PIC18F4550

Como uma breve recapitulação, devemos lembrar que interrupções são sinais emitidos por hardware ou software que interrompem a execução principal de um programa para tratar um evento específico. Elas permitem que o sistema responda imediatamente a certas condições, como ações de I/O, eventos de temporização ou outras condições de hardware, antes de retornar à execução normal.

O PIC18F4550 possui um sistema de interrupções que inclui uma variedade de fontes de interrupção, mas voltaremos nossa atenção para as externas e de temporização.

As interrupções externas são disparadas por eventos externos ao microcontrolador, como a mudança de estado em um pino de I/O, e muito comuns em aplicações que necessitam responder a ações do usuário (como pressionar um botão) ou a sinais de sensores. Já as de temporização ocorrem em resposta ao fenômeno de overflow do registrador de Timer (fenômeno já discutido anteriormente).

Essas interrupções, asssim como muitas outras, desencadeiam o que é chamado de ISRs, ou Rotina de Serviço de Interrupção, que são trechos de código executados após o ocorrimento de uma interrupção. As ISRs definem as respostas do microcontrolador aos eventos citados.

Look Up Table (LUT) para interação com o Display

Uma tabela de consulta (Look Up Table, LUT) é uma perfeita solução para a conversão de valores em decimal para o padrão correspondente num display de sete segmentos, principalmente se for implementada utilizando um array. Nesse caso, armazena-se em uma dada posição, o símbolo cujo padrão corresponde a representação do valor decimal do index correspondente.

Implementação

A implementação detalhada pode ser obtida observando o código src/main.c, mas destacamos pontos relevantes:

Valor Inicial do Timer

struct {
    volatile int high;
    volatile int low;
} timer;

Definiu-se uma struct global com dois campos (8bits cada), que representam o valor inicial do timer de 16bits.

// Some Code
// Sets period to 1s
        timer.high = 0x0B;
        timer.low = 0xDC;
// More Code

O valor do timer ao longo do código foi configurado como consta acima.

Configurações de Timer e Interrupções

// Setups Timer0
void setup_timer(void) { 
    T0CONbits.TMR0ON = 0b0; // Timer0 Turn OFF.
    T0CONbits.T08BIT = 0b0; // 16 bit counter.
    T0CONbits.T0CS = 0b0;   // Internal Instruction Cycle Clock (CLKO).
    T0CONbits.T0SE = 0b0;   // Low->High trigger.
    T0CONbits.PSA = 0b0;    // Enable Prescale.
    T0CONbits.T0PS = 0b100; // 1:32 Prescale.
}

O Timer0 foi utilizado como Timer e configurado como consta acima.

// Set up Global Interrupts
void setup_interrupts(void) {
    // General Interrupt configuration.
    INTCONbits.GIEH = 1;    // Enables High Priority Interrupts.
    INTCONbits.GIEL = 1;    // Enables Low Priority Interrupts.
    RCONbits.IPEN = 1;      // Enable priority levels for interrupts (handle both high and low).

    // Enable and priorizes timer interrupts
    ISR_TIMER_FLAG = 0;     // Clears Interrupt Timer Flag.
    INTCONbits.TMR0IE = 1;  // Enable Timer0 Interrupt.
    INTCON2bits.TMR0IP = 1; // Set Timer0 Interrupt as a High Priority Interrupt.

    // Enable and priorizes external interrupts.
    ISR_RB0_FLAG = 0;       // Clears Interrupt for RB0(INT0) Flag.
    ISR_RB1_FLAG = 0;       // Clears Interrupt for RB1(INT1) Flag.
    INTCON3bits.INT1IP = 0; // Set Priority INT1 to Low.
                            // Priority of INT0 is immutably High.
    INTCONbits.INT0IE = 1;  // Enable INT0 as External Interrupt.
    INTCON3bits.INT1IE = 1; // Enable INT1 as External Interrupt.

    // Sets interruption to rising edge trigger.
    INTCON2bits.INTEDG0 = 1;// rising edge for INT0.
    INTCON2bits.INTEDG1 = 1;// rising edge for INT1.
}

As configurações de interrupts constam acima. Destaca-se que a escolha de INT1 como prioridade baixa se deu por motivos de aprendizado (utilizar mais funcionalidades do PIC18F4550) do que por requisitos de aplicação.

//Some code
// High Interrupt Service Routine (Deals with INT0 and Timer0 Interrupt)
void high_isr(void) __interrupt(1) {
//Some code
//Low Interrupt Service Routine (Deals with INT1)
void low_isr(void) __interrupt(2) {
//Some code

As ISRs foram declaradas como consta acima. A diretiva de compilação __interrupt indica que se trata de uma interrupção e os códigos (1) High e (2) Low indicam o tipo.

#define ISR_RB1_FLAG INTCON3bits.INT1IF 
// Some Code
// Checks RB1 Interrupt
    if (ISR_RB1_FLAG == 1) {
        // More Code
        ISR_RB1_FLAG = 0; // Clears Interrupt Service Routine Flag
    }
// Other Code

Dentro das ISRs, cada trecho de interrupção verifica se o evento que desencadeou corresponde ao seu respectivo evento indicado pela flag, e caso seja, o trecho é executado e ao fim a flag de interrupt é limpa.

Detalhes sobre a LUT

unsigned int ssg_lut[10];
// Some Code
// Seven Segments LUT Configuration
// Macro for use the LUT
#define DISPLAY(VALUE) (\
    (VALUE >= 0 && VALUE <= 9)? ssg_lut[VALUE] : 0x00 \
)
//Other Code
SSG_DISPLAY = DISPLAY(counter); // Displays the counter at SSG Display
// More Code

A LUT utilizada foi implementada como um array, inicializado por uma função chamada configure_ssg_lut() e é chamada pela Macro Display que assegura que não haja busca de um index não populado.

Comparação com Implementação em Assembly 8051

O código em assembly anexo a esta seção foi desenvolvido para a mesma aplicação (com apenas algumas leves diferenças na especificação), mas visando o uso do microcontrolador 8051 ao invés de PICs da família 18.

As principais diferenças observadas no desenvolvimento de ambos os projetos foram:

Vantagens:

  • Maior legibilidade do código final:

Principalmente pela possibilidade do uso de Macros, Variáveis, Structs, Arrays e outras features da linguagem C que são facilmente compreendidas por outros programadores.

  • Desenvolvimento menos custoso:

Parte significativa do processo configuração do microcontrolador foi abstraída para o uso de diretivas de compilação, e configuração de Macros, o que facilitou o processo de implementação do código. Além disso, o uso das já citadas estruturas básicas da linguagem permitiram um desenvolvimento muito mais rápido e efetivo em relação ao projeto em Assembly 8051.

  • Possibilidade de Injeção de Assembly: Por fim, destaca-se que a possibilidade de utilizar Assembly diretamente no código permite que problemas críticos, envolvendo equívocos de compilação, sejam remediados com o uso direto de Assembly no projeto.

Desvantagens:

  • Menor eficiência de execução e espaço:

Durante o desenvolvimento do projeto em Assembly 8051, foi necessário uma otimização do uso de espaço em nível de precisar olhar para a memória de programa do 8051 diretamente durante o processo de desenvolvimento, o que produziu um código mais eficiente em perspectiva de espaço. Além disso, como o projeto foi desenvolvido diretamente no Assembly, todas as funcionalidades foram pensadas em termos das intruções da arquitetura, o que torna a execução da aplicação mais eficiente. O que não ocorreu com o projeto do PIC, em vista que otimização de memória e o ISA não foram levados em consideração durante o desenvolvimento.

Essa comparação pode ser realizada de forma mais clara observando o assembly produzido pelo projeto:

gpdasm -p p18f4550 -csno main.hex > asmfile.dis

Apesar de que a comparação direta entre Assemblys não ser justa por tratarem-se de duas arquiteturas distintas, a constatação ainda é válida, principalmente levando em conta que a abstração fornecida pelos compiladores geralmente vem associada a uma perda de desempenho, por menor que seja.

Conclusão:

O Projeto desenvolvido em Assembly do 8051, apesar de mais otimizado, acaba sendo uma solução menos atrativa que a adotada em Linguagem C, pois o uso de uma Linguagem de Programação propriamente dita permite um desenvolvimento mais facilitado e um resultado final mais compreensível para outros desenvolvedores que o código em Assembly. Além disso, a possibilidade do uso de Assembly diretamente em trechos do projeto permite que certas tarefas que requerem instruções especiais sejam realizadas sem perda de desempenho, o que traz o "melhor de ambos os mundos".

; Project Develop in 8051 Assembly for a Similar Application
ORG 0  ; Set start of program


MENU:               ; Start menu: this subroutine waits for the user to start the counter.
    JNB P2.0, MAIN  ; If user presses button SW0, then jump to MAIN subroutine.
    SJMP MENU       ; Do unconditional jump back to menu, if the user has not chosen to start.

    ORG 76H         ; Set address for Lookup Table
; Those are the hexadecimal values to display 0 to 9 on the 7-segment displays, in their respective order.
SEGMENT_TABLE: DB 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90


    ORG 2AH         ; Set address for MAIN subroutine
MAIN:               ; Set up Timer 0 for a 1-second delay
    MOV TMOD, #0x02 ; Set counter to MOD 0x02, in which the counter issues an interrupt 
                    ; every time that it overflows, and restarts automatically.
                    ; The counter register TL0 is reinitialized every cycle at the value stored in TH0.
    MOV TH0, #0x06  ; Set initial count value to 6, since we want to count from 0 to 250.
    MOV TL0, #0x06  ; Set counter register to 6, otherwise in the first cycle it will be set to zero.

; Configure interrupt
    SETB ET0        ; Enable Timer 0 interrupt 
    SETB EA         ; Enable global interrupts

; Start Timer 0
    SETB TR0        ; Starts counter
    MOV DPTR, #SEGMENT_TABLE ; 
    MOV R0, #0      ; Initialize register at value 0
    MOV R1, #0      ; Initialize register at value 0. This register stores time.
    MOV R2, #0      ; Initialize register at value 0. This one also stores time.
    MOV R3, #0      ; Initialize register at value 0. This register stores which number is being displayed
    CLR A           ; Clears accumulator
    MOVC A, @A+DPTR ; Move byte de código relativo ao DPTR para o acumulador
    MOV P1, A       ; Moves acumulator to pin 1, this makes the display to show the number 0.
    SJMP FASTER     ; Initializes A so that the counter updates every 0.25s

MAIN_LOOP:            ; MAIN_LOOP subroutine waits for the user to change the counting frequency.
    JNB P2.0, FASTER  ; If user presses the SW0 button, then revert back to updating every 0.25s by jumping to FASTER subroutine.
    JNB P2.1, SLOWER  ; If user presses the SW1 button, then start updating every 1s by jumping to SLOWER subroutine.
    SJMP MAIN_LOOP    ; If user does not press any button, continue waiting.

FASTER:             ; Soubroutine for faster counting: update every 0.25s
    MOV A, #3       ; Set A to the number 3
    SJMP MAIN_LOOP  ; Jump unconditionally to MAIN_LOOP

SLOWER:             ; Subroutine for slower counting: update every 1s
    MOV A, #15      ; Set A to the number 15
    SJMP MAIN_LOOP  ; Jump unconditionally to subroutine MAIN_LOOP

; Timer 0 interrupt handler
    ORG 0BH         ; Set Timer Interrupt subroutine address
TIMER0_ISR:
    INC R1                    ; Adds one to the first buffer
    CJNE R1, #0xFA, NOT_EQUAL ; If R1 not equal to 250, then go to subroutine NOT_EQUAL
    INC R2                    ; Increment second time buffer
    MOV R1, #0                ; Move the number 0 to R1
    MOV R0, A                 ; Store in R0 the content of the accumulator
    SUBB A, R2                ; Subtract value at R2 from A.
    JNC CONDITIONAL_OP        ; Jump to CONDITIONAL_OP if carry flag is equal to zero, in other words, when A is greater or equal to R2.
    MOV R2 ,#0                ; Otherwise, set R2 to zero and
    SJMP UPDATE_DISPLAY       ; Update display to show next number.

CONDITIONAL_OP:               ; This subroutine is an operation that is performed conditionally.
    MOV A, R0                 ; Move accumulator to R0

NOT_EQUAL:
    CLR TF0         ; Clear Timer 0 overflow flag 
    RETI            ; Return subroutine

    ORG 60H               ; Set Update Display subroutine address
UPDATE_DISPLAY:           ; Self-explanatory
    INC R3                ; Increments the number being displayed
    MOV A, R3             ; Moves offset to accumulator
    CJNE A, #10, DISPLAY  ; If R3 is different than 10, then jump to display subroutine immediately.
    MOV R3, #0            ; Go back to displaying zero, if R3 == 10.

DISPLAY:                  ; Displays number stored in R3 on the 7 segment display.
    MOV A, R3             ; Set accumulator to the value in R3
    MOVC A, @A+DPTR       ; Move byte de código relativo ao DPTR para o acumulador
    MOV P1, A             ; Displays the hexadecimal code stored in A on the display
    MOV A, R0             ; Return value
    SJMP NOT_EQUAL        ; Jump unconditionally to NOT_EQUAL
END