Close

The code - part 1 - Variables and Jumptables

A project log for Semyon

A small simon game using the 8 pin STC15F104W, written in 8051 assembly using the SDAS(ASXXXX) assembler from the SDCC toolchain.

hummusprinceHummusPrince 12/24/2019 at 23:497 Comments

Finally we've got there - a working version of Semyon! :D

I've incrementally built the code, starting with simply flashing the LED's, then flashing them according to a sequence stored in the memory, and when I've had a functioning game logic I only had to add a random sequence generation. All this history (and this first working version) can be found in the git repository of the project.

Let's have a look at the code.

Variables and Parameters assignment

Higher languages such as C hide from the user many many dirty details of their work. It's probably for the better. One of these details is assigning memory addresses to variables. However, writing in assembly, I have no such luxuries. Thus, I had to manually assign addresses to all the variables I use.

This way of work may seem inherently wrong to programmers who has only worked with high level languages, but strictly speaking about 8051 architecture, these MCUs were designed to be programmed that way. This is also why there are 4 switchable register banks, which are great for assembly programming. Compilers however don't use that feature well, if at all.

8051 was designd that way because compilers weren't as widespread as they are today - assembly was just the way one would program these things. I don't suspect the designers have believed that their design will be so popular and widespread, and refuse to die even 40 years after it's invention. That's also why there aren't any good, effective compilers for 8051, say a GCC port, despite the huge popularity of the ISA.

Moreover, the way I assign variables means that my variables will be global. This might pass shivers down the body of many programmers. Though it is technically possible to assign variables on the stack in assembly - that is what the compiler does, basically - this would be quite bulky and cumbersome, and given the fact that the stack is quite short anyway (and no heap to speak of without XRAM available), this is probably the original way to code this thing.

SFRs and parameters

Now to some code. First are SFRs which I use:

;SFRs
PCON2 = 0x97

This can seem weird, as the assembler should already recognize all of 8051's SFRs. Yet, each 8051 derivative adds new SFRs to the original design, which the assembler can't possibly know about.

Furthermore, some of the more liberal 8051 derivatives dumped some original peripherals, have altered their functionality, or have other SFRs located in the same addresses of the original ones. Timers are convenient victims - for example the STC15F104W has no T1 timer.

Thus I had to add myself the SFRs that I want to use. Specifically, in STC15 series, PCON2 controls the internal clock divider, which I wanted to modify sometime during development, and it's address is 0x97.

Later come parameters:

;Parameter values
P_LFSRMASK_L = 0x11
P_LFSRMASK_H = 0xa0

;State values
S_INITIALIZE = 0x00
S_DISPLAY_SEQUENCE = 0x01
S_GET_USER_INPUT = 0x02
S_GAME_OVER = 0x03
S_INVALID = 0xff

These are akin to /#define statements in C. These are simply constants which I use in the code. They have no manifestations as IRAM addresses as variables or SFRs have, but rather that of immediate values.

The state values are no different. These are the states of the state machine, which must be enumerated someway. Thus they are no different than parameters, except that their exact value doesn't really matter to me.

Variable and where (not) to find them

Then come the variables assignments:

;Variable addresses
V_LED_CNT = 0x30
V_LED_MAX = 0x31
V_STATE = 0x40
V_SEED_L = 0x20
V_SEED_H = 0x21

;Bool variables bit-addresses
RLED = P3.5
YLED = P3.4
GLED = P3.2
BLED = P3.3

The abstraction we have of variables in our minds boil down to simply a specific address in the memory which we tagged with a name and assigned a certain purpose to. The code is a lot more sensible when writing MOV V_STATE, #S_GET_USER_INPUT compared to MOV 0x40, #0x02 which isn't very meaningful to human beings.

The specific addresses chosen are quite arbitrary, with few exceptions:

The last point also means that recursive routines are a big no-no for any 8051 based MCU. I wanted to assign my stack base address to somewhere near 0x50, leaving me with a stack of 48 bytes. This should be plenty for Semyon - remember that original Simon programmers had only a single level stack to work with.

The last ones are the bit addresses of the GPIOs which drive the LEDs. 8051 has a distinct memory address space dedicated to bit variables with special operations such as SETB, CLR, CPL and so on.

Half of the bit-address space is mapped to the bits of the bytes in IRAM addresses 0x20-0x2F. The other half is mapped to some of the SFRs which are bit addressable, such as the port registers.

The assembler knows the standard P3.x addresses, so I just tagged them with the name I want to use.

The State Machine

As mentioned in the previous log, I've decided that the main function of Semyon will be an explicit state machine that governs the game logic:

main:
    ;This is the state machine that controls Semyon's logic.
    mov a, V_STATE
    s_initialize:
        cjne a, #S_INITIALIZE, s_display_sequence
        lcall initialize
        sjmp main
    s_display_sequence:
        cjne a, #S_DISPLAY_SEQUENCE, s_get_user_input
        lcall display_sequence
        sjmp main
    s_get_user_input:
        cjne a, #S_GET_USER_INPUT, s_game_over
        lcall get_user_input
        sjmp main
    s_game_over:
        cjne a, #S_GAME_OVER, s_invalid
        lcall game_over
        sjmp main
    
    s_invalid:
        mov V_STATE, #S_INITIALIZE
        ljmp main
        ;lcall reset

This thing is simply a C switch/case, as it may look in assembly. This is not the most pretty switch/case you've seen, but it is probably the simplest one - compare the value in question to the first immediate in your list, jump to the second one if it wasn't equal, and so on.

The s_invalid in this case is the default case.

Display LED colors with jumptables

A more elegant way to implement a switch/case statement, given that the cases are well ordered, is using a jumptable. That is, modifying PC ourselves using the value of the switch operand.

In 8051, the Program Counter register is not memory mapped. The only standard way I know to edit it is using the JMP opcode, which loads the value of A+DPTR to the PC register.

For Semyon I wanted to use only the bit variables of the GPIOs rather than the P3 SFR itself. I might change it in the future as it turned out not to be very elegant. This means no funny bit games with the P3 SFR, so I must represent each LED in some numerical way.

I chosen to translate two bits into an LED color. I have assigned it thus: Red = 00, Yellow = 01, Green = 10, Blue = 11. By adding this value to a certain DPTR, I can use JMP and find myself in one of 4 consecutive jump operations, which then jump me to where I really want to be. Take a look:

display_led:
    mov dptr, #led_jumptable
    mov a, r3
    anl a, #0x03
    rl a
    jmp @a+dptr
led_jumptable:
    sjmp light_rled
    sjmp light_yled
    sjmp light_gled
    sjmp light_bled
    
    light_rled:
        clr RLED
        lcall delay_display
        setb RLED
        lcall delay_display2
        ret
    light_yled:
        clr YLED
        lcall delay_display
        setb YLED
        lcall delay_display2
        ret
    light_gled:
        clr GLED
        lcall delay_display
        setb GLED
        lcall delay_display2
        ret
    light_bled:
        clr BLED
        lcall delay_display
        setb BLED
        lcall delay_display2
        ret

This is a popular compiler optimization which most compilers that honor themselves can utilize. This is also very good for situations where equal timing for all branches is needed. The repetitiveness of each branch in this particular example is not nearly the best code one can get though.

In the next part I'll talk about generating the (pseudo)random sequence and how to get user input (spoiler: button debouncing is a mandatory nag to tackle in such projects).

Discussions

Ken Yap wrote 12/28/2019 at 17:37 point

>where much simpler silicon was explicitly designed to support HLLs

The appearance of RISC architecture was years in the future after the release of the 8051. To put that year into context, in 1980, Unix was still owned by Bell Labs and only educational and research institutions could get a copy. It had just been ported to Digital Equipment's VAX-11/780 (a CISC architecture BTW). C was a SIL that only Unix programmers used, although in a few years, ports to other environments appeared thanks to the defiintion of the language in K&R.

A port of GCC to IA16 (search for it) was already a hard ask. GCC was intended from the start to target 32-bit and upwards processors. SDCC was in fact targetted at this gap in support for smaller devices and does a decent job.

A lot of the timeline gets forgotten in hindsight.

  Are you sure? yes | no

HummusPrince wrote 12/29/2019 at 00:01 point

AVR is 8 bit and is being targeted very well by GCC. It's a lot about AVR's internal design - using RISC does correlates with using compilers, as you seem to suggest.

However, without altering the timeline (I swear!), these MCUs I presented, although newer, aren't really RISC. They have no working registers, and barely have any ram. They resemble more of PIC12 parts than anything else I know. you can have a look at e.g. SN8P2501B, PMS150 or HT46R23.

The vendors still supply a C compiler because in contrast to the cheerful 80's - almost no one today is willing to use assembly.

  Are you sure? yes | no

Ken Yap wrote 12/29/2019 at 00:06 point

As I have already explained C really needs a functional stack. Also check that the C provided for these MCUs are not subsets of C. They may have sacrificed some features like reentrancy, like SDCC did for the 8051.

  Are you sure? yes | no

Ken Yap wrote 12/27/2019 at 01:01 point

>8051 was designd that way because compilers weren't as widespread as they are today

Actually it was designed that way because it would have been a waste of design resources to create silicon that supported HLLs well, given that these MCUs were used for (relatively) small applications, and even if they did manage to design for HLLs the product price would have been too much for the target market. So what if the engineer was inconvenienced writing in assembly because once tested the chips will be used in the millions, e.g. microwave ovens.

Compilers were already widespread, just not in MPU and MCU circles, until the silicon technology became cheap enough to support decent architectures. You have to remember that even MPUs and MCUs were relatively expensive items for hobbyists at the beginning. Now they are dirt cheap.

>aren't any good, effective compilers for 8051, say a GCC port

The 8051 architecture is simply not adequate for supporting the full feature set of C so nobody wants to try to target gcc to it. As you note even stack variables are a hard ask for the 8051 and SDCC uses static locals by default.

These days the silicon technology has caught up with the HLL requirements and there are lots of cheap MCUs that support C well, but the 8051 survives in derivatives, and also where old code has to continue to run.

  Are you sure? yes | no

HummusPrince wrote 12/27/2019 at 11:12 point

>it was designed that way because it would have been a waste of design resources to create silicon that supported HLLs well

I don't think that saying that it was waste of silicon is quite accurate. Even for microwave ovens, using a HLL is cheaper and faster == less programmer hours == cheaper product.

Today even the cheapest MCUs, OTP chips for mass production by milions only, say Holtek or Sonix 8-bitters, come with a C compiler. Even the Padauk MCUs, some of which cost 2.5 cents only - almost cheaper than the sand it was made from - come with a pseudo C compiler.

Despite being newer than 8051, these are simpler machines - accumulator based too, no address-map for bits (no CPL operations for instance), utilize only skip instructions for conditional branching (forget about fancy CJNZ stuff), almost always smaller SRAM and ROM.

These thing definitely are simpler and more primitive than 8051 (And that's leaving aside all the XRAM/extended ROM 8051 originally supported), yet all of them were intended to be programmed using a C compiler.

The only explanation I have for this situation is that 8051 was simply designed with assembly programming in mind.

>Compilers were already widespread

Well, not as today, right? Having compilers for mainframes or minicomputers doesn't make them widespread. Writing bare metal assembly wasn't something special back then as it is today. It's also true in the then-new world of PC - for example, MS-DOS was written in x86 assembly.

Today with the advance of technology (and may I say that the open source community and the internet contributed a lot to it), no one would dare releasing a platform which has no compiler of some sort. And it's probably for the better.

> nobody wants to try to target gcc to it.

I'm not very well-versed in compiler shenanigans so I might be wrong in this one.

Anyway from what I've heard it is probably kinda impossible to port GCC to 8051 - it's limited resources and funky memory architecture aren't digested well in GCCs internals, and the accumulator based design is probably the last nail in MCS51-GCC's coffin.

It seems to me that there's a huge market for a 8051 GCC port, so the interest to port it should be real. Simply it is not going to work.

  Are you sure? yes | no

Ken Yap wrote 12/27/2019 at 12:22 point

I didn't say a waste of programmer resources, I said a waste of design resources. You would have to put more silicon resources to support a HLL. If it makes a difference between a chip that costs $10 vs $20, then the $10 design will win. The 68k is a better architecture than the 8086 but the latter was cheaper so guess which one was picked for the IBM PC.

Also it was not generally the case then that hardware programmers were used to HLLs. C wasn't widely known until commercial versions of Unix were released. And so what if the programmer spends thrice the time writing assembly if the product gets stamped out in the millions once it's working. The extra cost is made up in the cheaper chips. Of course if there is a HLL and there is no penalty in more expensive chips, then go for it. The 8051 was not "designed for assembly". Rather C was not considered when it was designed. It was basically a souped up version of the 8048. There was an Intel language called PL/M which apparently could target 8051s.

Compilers not widespread back then? What do you think the COBOL, FORTRAN, Pascal, etc programmers were using? Back then microprocessors were only a minuscule fraction of the IT market. Mainframes ruled the world, until minis came along, then when micros became as powerful as minis, the desktop revolution happened. Then when chips got cheap enough, the maker revolution happened.

It's easy for you today to posit a particular history but that's not how it was. You had to be there.

The main barrier to reentrant C code for the 8051 is the lack of a true stack and indexed addressing off the stack, needed for locals and parameters. Some of the 80x51 descendants are better in this regard. Small memory size, or accumulator instead of many registers is not fatal to supporting C. Neither is the lack of bit instructions or skip vs branch. The C standard doesn't state how bit variables are to be supported. But C does require a real stack if you want reentrant code.

For a given chip you also have to be careful to check the level of C language supported. It may be a subset, but then a subset may be adequate for the application. In fact I wonder why you didn't use C for your project. Everything you want to do can be done in SDCC's subset of C, perhaps with some inline asm. Extra register banks are generally only used for fast context switch and some inline asm in interrupt handlers would suffice.

  Are you sure? yes | no

HummusPrince wrote 12/28/2019 at 14:10 point

I got what you say about design resources, but given plenty of examples where much simpler silicon was explicitly designed to support HLLs, I can't accept that argument.

The only reason for 8051's bad C support I can think of is, as you say yourself, is that no one cared that the programmers will use assembly - not because companies like to enlarge development cost and time, but rather because it's the most straightforward and popular way to do it back then, which we seem to agree on.

I never heard of this PL/M port though. While I don't have any evidence, the compiler seems to me more like an afterthought of Intel rather than support for it was a design goal of the chip itself.

I never said that compilers didn't exist, just that they weren't as used as they are today, which I'm quite sure is right, though I wasn't here when such dinosaurs roamed the computing lands. Anyway, you are right that it might be better to add in the log itself that I consider mainly microcontrollers in this context.

Also, I strictly considered GCC in my comment. The restrictive architecture of 8051s don't mean no support for C, just that a GCC port is not to be expected, despite how awesome it would be to have such port.

> In fact I wonder why you didn't use C for your project.

This is an educational project, where one of the main goals is to write full programs in assembly, close to the metal. Using SDCC may happen in future projects :)

  Are you sure? yes | no